JVM類載入機制與類初始化順序

讓蛋蛋飛發表於2018-08-19

類載入機制

類從被載入到虛擬機器記憶體開始,到解除安裝出記憶體為止,它的生命週期包括7個階段:載入、驗證、準備、解析、初始化、使用和解除安裝。

一、載入

在載入階段,虛擬機器完成3件事:

  1. 通過一個類的全限定名來獲取定義此類的二進位制位元組流。
    Java虛擬機器沒有指明二進位制位元組流要從一個Class檔案中獲取,也可以從別的地方獲取:
    從ZIP包中讀取
    從網路中獲取
    執行時計算生成(Java動態代理技術)
    由其他檔案生成(由JSP檔案生成對應的Class類)
    從資料庫中讀取
    ......
    複製程式碼
  2. 加二進位制位元組流儲存在方法區中(按照虛擬機器所需的格式儲存)。
  3. 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。
    Class物件比較特殊,存放在方法中(HotSpot虛擬機器)
    複製程式碼

二、驗證

驗證的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

一般包括兩個方面:

  1. 格式語義校驗:
    例如:
    是否以0xCAFEBASE開頭
    主、次版本號是否在當前虛擬機器處理範圍內
    ......
    複製程式碼
  2. 程式碼邏輯校驗

三、準備

準備階段正式為靜態變數分配記憶體並設定初始值,這些靜態變數在方法區中分配記憶體。

注意:

  1. 準備階段,JVM只會為靜態變數(static修飾)分配記憶體,不包括例項變數,例項變數將會在物件例項化時隨物件一起分配在Java堆中。
    準備階段,只會為value分配記憶體,不會為name分配記憶體
    public static int value = 123;
    private String name = "Tom";
    複製程式碼
  2. 設定初始值:
  • 通常情況(無final):零值

    準備階段,未執行任何Java方法,而value賦值為123指令是程式編譯後,存放於類構造器方法中,在初始化階段才會執行,因此準備階段,會設定零值。

    準備階段,會設定零值
    public static int value = 123;
    複製程式碼
  • 特殊情況(有final):實際值

    常量,準備階段會設定實際值123
    public static final int value = 123;
    複製程式碼

四、解析

解析階段是虛擬機器將常量池內的符號引用替換為直接在其記憶體中的直接引用的過程。

JVM主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫點限定符7類符號引用進行解析。

五、初始化

到了初始化階段,才真正開始執行類中定義的Java程式程式碼。

JVM規定有且僅有5種情況必須對類進行初始化:

  1. 遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。

    對應場景為:

    • 使用new關鍵字例項化物件
    • 讀取或設定一個類的靜態欄位(final修飾的除外)
    • 呼叫一個類的靜態方法
  2. 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行初始化,則需要先觸發其初始化。

  3. 當初始化一個類時,需要先初始化父類。

  4. 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的類),虛擬機器會先初始化這個類。

  5. 當使用JDK1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行過初始化,則需要先觸發其初始化。

六、使用

當JVM完成初始化階段之後,JVM便開始從入口方法開始執行使用者的程式程式碼

七、解除安裝

當使用者程式程式碼執行完畢後,JVM便開始銷燬建立的Class物件,最後負責執行的JVM也退出記憶體。


類的初始化順序

實際上Java程式碼編譯成位元組碼後,沒有構造方法的概念,只有類初始化方法和物件初始化方法。

1. 類初始化方法:

編譯器會按照其出現順序,收集靜態變數的賦值語句、靜態語句塊、最終組成類初始化方法。

類初始化方法一般在類初始化的時候執行。

注意:

  • 靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但不能訪問。
    靜態語句塊可以為i賦值,但不能訪問。
    public class Test {
        static {
            i = 0;
            System.out.print(i);
        }
        static int i = 1;
    }
    複製程式碼
  • 對於靜態變數,只有直接定義這個靜態變數的類才會被初始化(執行靜態程式碼塊),因此通過子類來引用其父類中定義的靜態變數,只會觸發父類的初始化而不會觸發子類的初始化。

2. 物件初始化方法:

編譯器會按照其出現順序,收集例項變數的賦值語句、普通程式碼塊、最後收集建構函式的程式碼,最終組成物件初始化方法。

物件初始化方法一般在例項化物件的時候執行。

總結:

一個類的初始化順序:

1. 初始化靜態變數初始值

類載入的準備階段,JVM會為靜態變數初始化零值,為常量(final static修飾)初始化實際值。
複製程式碼

2. 初始化入口方法

類載入的初始化階段,JVM會尋找整個main方法入口,初始化main方法所在的整個類。
複製程式碼

3. 執行類初始化方法

JVM會按照其出現的順序收集靜態變數的賦值語句、靜態程式碼塊、最終組成類初始化方法,由JVM執行。
複製程式碼

4. 執行物件初始化方法

JVM會按照收集例項變數賦值語句、普通程式碼塊、最後收集構造方法,將它們組成物件初始化方法,由JVM執行。
複製程式碼

如果在初始化main方法所在類的時候遇到了其他類的初始化,則先載入對應的類,載入完成後返回,最終返回main方法所在類。

相關文章