類加載過程

以前在網上看到一道面試題,很形象的描述了類的加載初始化過程。要徹底理解這道題,就不得不深刻理解類加載的過程。面試題以下:html

class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;
 
    private SingleTon() {
        count1++;
        count2++;
    }
 
    public static SingleTon getInstance() {
        return singleTon;
    }
}
 
public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

這道題的正確答案爲 :java

count1=1面試

count2=0數組

至於爲何會是這個答案,這就涉及到了 JVM 類加載的過程。安全

類的生命週期

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括加載、驗證、準備、解析、初始化、使用和卸載 7 個階段,其中驗證、準備和解析 3 個階段統稱爲鏈接,這 7 個階段發生的順序以下圖所示。網絡

類的生命週期

加載、驗證、準備、初始化和卸載這 5 個階段的順序是肯定的。解析階段則不必定,因爲支持運行時綁定,類能夠在初始化以後再開始進行解析。同時這些階段只是按照順序進行開始,並不必定會按照順序進行或者結束,由於這些階段一般都是互相交叉地混合式進行的,一般會在一個階段執行過程當中調用、激活另一個階段。數據結構

類的加載過程

加載

加載是類加載過程的一個階段,是根據特定名稱查找類或接口類型的二進制表示(binary representation),並由此二進制表示來建立類或接口的過程。在加載階段,虛擬機須要完成 3 件事:多線程

  1. 經過一個類的全限定名來獲取定義此類的二進制字節流。
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  3. 在內存中生成一個表明這個類的 java.lang.Class 對象,做爲方法區這個類的各類數據的訪問入口。

加載途徑

虛擬機加載的是類的二進制流,只是對內容格式作了限制,並無指名要從哪裏去獲取、怎樣獲取一個類的二進制流,比較常見的有一下幾種:oracle

  • 從 jar、ear、war 包中讀取。
  • 從網絡流中讀取,這種場景的典型應用就是 Applet。
  • 運行時計算生成,使用最多的就是動態代理技術。
  • 其餘文件生成,典型的場景是 JSP 應用,即由 JSP 文件生成對應的 Class 類。

加載方式

  • 對於一個非數組類,加載階段可使用系統提供的引導類加載器來完成,也能夠由用戶自定義的類加載器去完成。
  • 數組類是 Java 虛擬機直接建立的。一個數組類建立的過程遵循如下規則:
    • 數組的元素是引用類型,那就遞歸採用本文所講的類加載過程加載這個元素,數組類將在加載該元素類型的類加載器的類名稱空間上被標識。
    • 若是數組的元素類型不是引用類型(例如:int[] 數組),Java 虛擬機將會把數組類標記與引導類加載器關聯。
    • 數組的可見性與他的元素類型的可見性一致,若是元素類型不是引用類型,那數組的可見性將默認爲 public。

加載階段完成後,二級制字節流就按照虛擬機所需的格式儲存在方法區之中,而後在內存中實例化一個 java.lang.Class 對象,將這個對象做爲程序訪問方法區中的這些類型數據的外部接口。jvm

驗證

驗證時鏈接階段的第一步,這一步是爲了保證 Class文件二進制字節流符合虛擬機規範,而且不會危害虛擬機自身的安全。Java 虛擬機規範有大量的約束和驗證規則,詳細的描述的驗證過程。驗證過程主要仍是圍繞 Class 文件格式對各部分進行驗證。Class 文件格式課參考另外一篇博文字節碼文件結構詳解。但從總體上看,驗證階段大體會完成下面 4 個階段的驗證動做。

文件格式驗證

第一階段要驗證字節流是否符合 Class文件格式規範,而且能被當前版本的虛擬機處理。這一階段可能包括下面驗證點:

  • 是否一魔數 0xCAFEBEBE 開頭。
  • 主次版本號是否在當前虛擬機處理範圍以內。
  • 常量池中的常量是否有不被支持的常量類型(檢查常量 tag 標誌)。
  • 指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量。
  • .......

第一階段的驗證遠不止這些,該階段的主要目的是保證輸入的字節流能正確的解析並存儲於方法區內。這階段的驗證是基於二進制字節流進行的,只有經過了這個階段的驗證後,字節流纔會進入內存的方法區中進行存儲,因此後面的3個驗證階段所有是基於方法區的存儲結構進行的,不會再直接操做字節流。

元數據驗證

元數據驗證是對字節碼描述的信息進行語義分析,確保其描述的信息符合 Java 語言規範的要求,這個階段可能包括的驗證點以下:

  • 這個類是否有父類(出了 java.lang.Object 以外,全部的類都應當有父類)。
  • 這個類是否繼承了不容許被繼承的類(被 final 修飾的類)。
  • 若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法。
  • 類中的字段、方法是否與父類產生矛盾(例如覆蓋了父類的final字段,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不一樣等)。

字節碼驗證

字節碼驗證將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的事件。

  • 保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做。
  • 保證跳轉指令不會跳轉到方法體之外的字節碼指令上。
  • 保證方法體中的類型轉換是有效的。

若是一個方法經過了字節碼驗證,也不能說明其必定就是安全的。

符號引用驗證

符號引用驗證能夠看作是對類自身之外(常量池中的各類符號引用)的信息校驗,一般須要校驗一下內容:

  • 符號引用中經過字符串描述的全限定名是否能找到對應的類。
  • 在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。
  • 符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當前類訪問。

若是一個類沒法經過符號引用驗證,那麼將會拋出一個java.lang.IncompatibleClassChangeError異常的子類,如常見的java.lang.IllegalAccessErrorjava.lang.NoSuchFieldErrorjava.lang.NoSuchMethodError等。

準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些類變量所使用的內存都將在方法區中進行分配。此處須要明確類變量的含義,即被static修飾的變量,而不包括實例變量,實例變量會在初始化階段隨着對象一塊兒分配在 Java 堆中。此時分配的初始值是數據類型的零值,並非咱們定義的初始值。此處還要明確一個概念,若是變量被final修飾,則此字段的字段屬性表存在 ConstantValue 屬性,那麼在準備階段變量就會被初始化爲 ConstantValue屬性所指定的值。可經過下例代碼來對照理解:

public static int a = 10;
public static final int B = 20;

其部分彙編字節碼爲:

Constant pool:
  #2 = Fieldref           #3.#21         // tech/techstack/blog/Test.a:I
  #3 = Class              #22            // tech/techstack/blog/Test
  #5 = Utf8               a
  #6 = Utf8               I
  #21 = NameAndType        #5:#6          // a:I
  #22 = Utf8               tech/techstack/blog/Test


public static int a;
  descriptor: I
  flags: ACC_PUBLIC, ACC_STATIC

public static final int B;
  descriptor: I
  flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  ConstantValue: int 20

static {};
  descriptor: ()V
  flags: ACC_STATIC
  Code:
    stack=1, locals=0, args_size=0
       0: bipush        10
       2: putstatic     #2                  // Field a:I
       5: return
    LineNumberTable:
      line 8: 0

從上述代碼能夠看出,B 字段對應的 field_info 與 a 字段對應的 field_info 相比對了一個 Constant_Value 屬性,而 Constant_Value 屬性的值 20 就會在準備階段直接賦給字段 B。同時在字節碼第 19 行有一個 static {};方法,此方法對應的就是類的構造方法<clinit>在初始化階段執行,它的Code屬性中對應的字節碼指令bipush 10爲往操做數棧壓入 10,putstatic 則是將值 10 賦值給 a 字段。

基本數據類型的零值:

數據類型 零值
byte (byte)0
char '\u0000'
short (short)0
boolean false
int 0
long 0l
float 0.0f
double 0.0
reference null

解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,符號引用在 Class 文件中以 CONSTANT_Class_info、CONSTANT_Feildref_info、CONSTANT_Methodref_info 等類型的常量出現,具體能夠參考博文字節碼文件結構詳解。此處有符號引用和直接引用兩個概念須要瞭解一下.

符號引用(Symbolic References)

符號引用以一組符號來描述所引用的目標,符號引用能夠是任何形式的字面量,只要使用能無歧義地定義到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標不必定已經加載到內存中。各類虛擬機實現的內存佈局能夠各不相同,可是它們能接受的符號引用必須都是一致的。符號引用的字面量形式須要明確的定義在 Class 文件格式中。

直接引用(Direct References)

直接引用能夠是直接執行目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不一樣的虛擬機實例上翻譯出來的直接引用通常不會相同。若是有了直接引用,那引用目標一定已經出如今內存中。

關於符號引用和直接引用兩個概念看起來很空洞,此處放一個 R 大的回答:傳送門

虛擬機規範並未規定解析發生的具體時間,只要求在執行anewarraycheckcastgetfieldgetstaticinstanceofinvokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtualldcldc_wmultianewarraynewputfieldputstatic這 16 個用於操做符號引用的字節碼指令以前,先對他們所使用的符號引用進行解析。因此虛擬機實現能夠根據須要來判斷究竟是在類被加載和加載時就對常量池中的符號引用進行解析仍是等到一個符號引用將要被使用前纔去解析它。

加載過程當中的解析階段爲靜態的將符號引用替換爲直接引用的過程。可與虛擬機棧內存中的動態連接參照記憶。

解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符 7 類符號引用進行,分別對應於常量池的CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_infoCONSTANT_MethodType_infoCONSTANT_MethodHandle_infoCONSTANT_InvokeDynamic_info 7種常量類型。

初始化

初始化階段是類加載過程當中的最後一步,此階段纔是真正執行類中定義的 Java 程序代碼。初始化階段和準備階段的初始化是不一樣概念的,準備階段的初始化是給類字段賦值零值的過程,而類加載過程當中的初始化階段能夠看作是類對象的初始化。對於類的初始化反映到字節碼中就是類的<clinit>()方法。從另一個角度來說,能夠將初始化階段理解成是執行類構造器<clinit>()方法的過程。同時對於<clinit>()方法,有幾個概念要弄清楚。

  • <clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static {})中的語句合併產生的。
  • 編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在靜態語句塊中能夠賦值,但不能訪問。
  • 在執行子類的<clinit>()方法以前,虛擬機會確保子類的<clinit>()方法已經執行完畢。所以在虛擬機中第一個被執行的<clinit>()方法的類確定是java.lang.Object。
  • 接口中不能使用靜態代碼塊,但仍有變量初始化的賦值操做,所以接口也會生成<clinit>()方法。與類不一樣的是,執行接口<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。接口的實現類在初始化時也不會執行接口的<clinit>()方法。
  • 在多線程的狀況下,虛擬機會保證一個類的<clinit>()方法只被一個線程調用,其它線程會被阻塞。同時,在一個類加載器下,一個類的<clinit>()方法只會被執行一次。

注:

本文所說的類對象與類實例不是一個概念。關於類對象與類實例以及 java.lang.Class 對象之間的關係,此處能夠引用 R 大的一個回答傳送門

在HotSpot VM中,對象(類的實例對象)、類的元數據(InstanceKlass)、類的Java鏡像(java.lang.Class 實例),三者之間的關係是這樣的:

Java object      InstanceKlass       Java mirror
[ _mark  ]                          (java.lang.Class instance)
[ _klass ] --> [ ...          ] <-\              
[ fields ]     [ _java_mirror ] --+> [ _mark  ]
          [ ...          ]   |  [ _klass ]
                             |  [ fields ]
                              \ [ klass  ]

每一個Java對象的對象頭裏,_klass字段會指向一個VM內部用來記錄類的元數據用的InstanceKlass對象;InsanceKlass裏有個_java_mirror字段,指向該類所對應的Java鏡像——java.lang.Class實例。HotSpot VM會給Class對象注入一個隱藏字段「klass」,用於指回到其對應的InstanceKlass對象。這樣,klass與mirror之間就有雙向引用,能夠來回導航。這個模型裏,java.lang.Class實例並不負責記錄真正的類元數據,而只是對VM內部的InstanceKlass對象的一個包裝供Java的反射訪問用。

經過上面的引用,能夠清楚的知道 Java Object, InstanceKlass, Java mirror(java.lang.Class instance)在內存中的分佈了。

對於初始化階段能夠經過代碼來理解一下:

public class SuperClass {

    public static int superClassField = 1;

    static {
        System.out.println("supper class static code");
    }

    public SuperClass() {
        System.out.println("supper class constructor");
    }
}

public interface SuperInterface {
    int superInterfaceField = 10;

}

public class SubClass extends SuperClass implements SuperInterface {

    public static int subClassField = 20;

    static {
        System.out.println("sub class static code.");
    }

    public SubClass() {
        System.out.println("sub class constructor");
    }
}

public class TestClassLoad {
    static {
        System.out.println("test class load");
    }
}

public class App {
  
    static {
        System.out.println("App main static code");
    }
  
    public static void main(String[] args) {
        System.out.println(SubClass.superClassField);
        System.out.println("----------------------");
        new Thread(SubClass::new).start();
    }
}

在運行 SubClass 的時候加上 -XX:+TraceClassLoading 參數,打印出來運行過程當中加載的類。上述代碼運行結果爲結果 1:

// 類加載日誌(節選)
[Loaded tech.stack.App from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperInterface from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SubClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]

App main static code
supper class static code
1
----------------------
sub class static code.
supper class constructor
sub class constructor

註釋掉new Thread(SubClass::new).start();從新運行程序,獲得一下輸出結果 2:

[Loaded tech.stack.App from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperInterface from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SubClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]

App main static code
supper class static code
1
----------------------

而後將 System.out.println(SubClass.superClassField); 替換爲 System.out.println(SubClass.subClassField); 再次運行程序,獲得輸出結果 3:

[Loaded tech.stack.App from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperInterface from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SubClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]

App main static code
supper class static code

sub class static code.
20
----------------------

這幾段代碼信息量很大,根據上文所講慢慢分析:

  • 加載
    • 從類加載日誌從看到TestClassLoad類始終都沒有被加載。而AppSuperInterfaceSuperClassSubClass 始終被加載,是否是能夠證實屬於 Applicatin 做用域範圍內的類會在首次使用時加載。
    • 對比結果 1 和結果 2 以及沒有顯示調用SuperInterface任何方法、變量能夠看出對於子類來講,在加載子類時首先要加載實現的接口以及父類。
  • 初始化
    • 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

    • 結果 1 結果 3 都代表父類的<clinit>()方法在子類<clinit>()方法以前調用。

    • 結果 1 結果 2 對比代表經過子類調用父類的靜態的變量只會引發父類的初始化並不會使子類初始化。

    • 對比結果 1 和結果 2 說明在多線程的狀況況下只要類加載器相同,類只初始化一次。

    • 對比結果 一、二、3 能得出一個實例的初始化順序

      1. 父類 static 代碼塊即父類的<clinit>()方法。
      2. 子類的 static 代碼塊即子類的<clinit>()方法。
      3. 父類的構造方法即父類的<init>()方法。
      4. 子類的構造方法<init>()方法、

注:

  1. 關於類實例的初始化過程即對象的實例化過程會專門在另外一篇博客進行講解。

  2. 關於"接口中不能使用靜態代碼塊,但仍有變量初始化的賦值操做,所以接口也會生成<clinit>()方法。" 在接口中變量初始化賦值操做可參考以下代碼:

    public interface SuperInterface {
        int superInterfaceField = 10;
    
        SuperClass su = new SuperClass();
    
    }
    
    // bytecode
    
    {
      public static final int superInterfaceField;
        descriptor: I
        flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
        ConstantValue: int 10
    
      public static final tech.stack.SuperClass su;
        descriptor: Ltech/stack/SuperClass;
        flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=2, locals=0, args_size=0
             0: new           #1                  // class tech/stack/SuperClass
             3: dup
             4: invokespecial #2                  // Method tech/stack/SuperClass."<init>":()V
             7: putstatic     #3                  // Field su:Ltech/stack/SuperClass;
            10: return
          LineNumberTable:
            line 10: 0
    }

類的加載時機

關於類在何時加載,咱們能夠有上面的代碼窺見一斑。可是這只是在JDK1.8, Hotspot 虛擬機測試的狀況下得出的結論,也不必定會是正確的,由於 Java 虛擬機規範中並無進行強制約束,關於加載階段,都是根據虛擬機的具體實現來自由把握。可是對於初始化階段,虛擬機嚴格規定了有且只有 5 種狀況必須當即對類進行初始化(而加載、驗證、準備天然須要再次以前開始):

  1. 遇到newgetstaticputstaticinvokestatic這4條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。生成這4條指令的最多見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候(已經過上文代碼驗證)。
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。
  3. 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化(上文代碼也已驗證)。
  4. 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類(上文代碼也已驗證)。
  5. 當使用JDK 1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。

嘗試着補充解釋一下這幾條其中的原理,對於 new 關鍵字天然不用多說,new 關鍵字實例化類的實例對象以前天然會執行類的初始化操做,以完成 Java 程序對類的一些操做。getstatic putstatic 指令的含義爲讀取或設置一個類的靜態字段,此處仍是應用R大的回答,原文與上處引用出自同一處:

從JDK 1.3到JDK 6的HotSpot VM,靜態變量保存在類的元數據(InstanceKlass)的末尾。而從JDK 7開始的HotSpot VM,靜態變量則是保存在類的Java鏡像(java.lang.Class實例)的末尾。假若有這樣的A類:

class A {
static int value = 1;
}

那麼在JDK 6或以前的HotSpot VM裏:

Java object      InstanceKlass       Java mirror
[ _mark  ]                          (java.lang.Class instance)
[ _klass ] --> [ ...          ] <-\              
[ fields ]     [ _java_mirror ] --+> [ _mark  ]
             [ ...          ]   |  [ _klass ]
             [ A.value      ]   |  [ fields ]
                                 \ [ klass  ]

能夠看到這個A.value靜態字段就在InstanceKlass對象的末尾存着了。而在JDK 7或以後的HotSpot VM裏:

Java object      InstanceKlass       Java mirror
[ _mark  ]                          (java.lang.Class instance)
[ _klass ] --> [ ...          ] <-\              
[ fields ]     [ _java_mirror ] --+> [ _mark   ]
             [ ...          ]   |  [ _klass  ]
                                |  [ fields  ]
                                 \ [ klass   ]
                                   [ A.value ]

能夠看到這個A.value靜態字段就在java.lang.Class對象的末尾存着了。

據此咱們應該就能得出結論,在設置靜態變量的時候已經須要根據InstanceKlass生成java.lang.Class對象了,而靜態變量已經不能在方法區經過讀取類元信息進行獲取或者儲存。而生成 Java mirror 必然要經過完整的類元信息,所以須要進行初始化動做。對於java.lang.reflect包的反射方法,其根據的就是 java.lang.Class對象。對於子類初始化時,由於 Java 的繼承特性,繼承的是父類完整的類信息。父類進行初始化也是理所固然的。

上述 5 種場景中的行爲稱爲對一個類的主動引用。除此以外,全部的引用類的方式都不會觸發初始化,稱爲被動引用。例:

  • 經過子類調用父類的靜態字段(變量+常量),不會致使子類的初始化。代碼可參考上文。

  • 經過數組定義來引用類,不會觸發此類的初始化

    public class App {
    
        public static void main(String[] args) {
          SuperClass[] superClasses = new SuperClass[10];
        }
    }
  • 常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化。

    public class ConstantClass {
        public static final String HELLO_WORLD = "hello world !";
    }
    
    public class App {
    
        public static void main(String[] args) {
            System.out.println(ConstantClass.HELLO_WORLD);
        }
    }

    這是由於雖然在Java源碼中引用了ConstClass類中的常量HELLOWORLD,但其實在編譯階段經過常量傳播優化,已經將此常量的值"hello world !"存儲到了App類的常量池中,之後App對常量HELLO_WORLD的引用實際都被轉化爲App類對自身常量池的引用了。也就是說,實際上App的Class文件之中並無ConstantClass類的符號引用入口,這兩個類在編譯成Class以後就不存在任何聯繫了。能夠看一下App的字節碼。

    Constant pool:
    	 #4 = String             #25            // hello world !
    	 #25 = Utf8               hello world !
    
    {
     
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #4                  // String hello world !
             5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 10: 0
            line 11: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  args   [Ljava/lang/String;
    }

例題解析

其實講到這裏,這道題也算是分析完了,那麼就根據上面所講,總結一下這道題:

  1. 運行 Test 類的 main 方法,回顧上文確定要先加載、驗證、初始化 Test 類(因爲加載、驗證必然發生在初始化以前,下面分析就忽略這兩個階段)。
  2. SingleTon.getInstance()Test 類調用 SingleTon 類的靜態方法,必然引發 SingleTon 類的初始化。
  3. SingleTon 類存在 singleTon count1 count2 三個靜態變量,所以這三個靜態變量會被編譯器順序收集值到<clinit>()方法中。
  4. <clinit>() 開始就是 new SingleTon() 會建立 SingleTon 類的實例 singleTon,此時 ``singleTon.count1 singleTon.count2` 值都爲 1。
  5. <clinit>() 操做完第一個變量 singleTon 以後即是對第二個變量 count1 操做,此時就會將 1 賦值給 SingleTon 變量 count1
  6. <clinit>() 後續操做即是執行 count2 = 0 即經過操做數棧將 0 賦值給SingleTon 變量 count2

查看SingleTon 的字節碼:

{
 static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #5                  // class tech/stack/SingleTon
         3: dup
         4: invokespecial #6                  // Method "<init>":()V
         7: putstatic     #4                  // Field singleTon:Ltech/stack/SingleTon;
        10: iconst_0
        11: putstatic     #3                  // Field count2:I
        14: return
      LineNumberTable:
        line 4: 0
        line 6: 10
}

static{} 方法執行流程正如上文分析。不妨想一下若是將private static SingleTon singleTon = new SingleTon();移動到public static int count2 = 0;下面將會輸出什麼結果?

總結

類加載

參考:

[1] 周志明.深刻理解Java虛擬機:JVM高級特性與最佳實踐.北京:機械工業出版社,2013.

[2] Chapter 5. Loading, Linking, and Initializing

[3] JVM裏的符號引用如何存儲?

[4] JVM符號引用轉換直接引用的過程?

文章首發於陳建源的博客,歡迎訪問。
文章做者:陳建源
文章連接: https://www.techstack.tech/post/lei-jia-zai-guo-cheng/

相關文章
相關標籤/搜索