JVM學習記錄-類加載時機


虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是類的加載機制。

在Java語言里面,類型的加載、連接和初始化過程都是在程序運行期間完成的。類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:

加載(Loading)、驗證(Verification)、准備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中哦驗證、准備、解析3個部分統稱為連接(Linking),這7個階段順序如下圖:

其中加載、驗證、准備、初始化、和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始(這里僅僅指的是開始,而不是按部就班的進行完成,是因為這些階段通常都是相互交叉的進行的,通常在一個階段執行的過程中調用、激活另外一個階段),而解析階段則不一定,它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時綁定。

那么在什么時候開始類加載過程的第一個階段(也就是加載)呢?Java虛擬機規范中並沒有進行強制約束,這點虛擬機根據自身實現來把握。但對於初始化階段,虛擬機規范則是嚴格規定了有且只有5中情況必須立即對類進行初始化(加載,驗證,准備肯定要在此之前進行了)。

  1. 創建類實例的時候,讀取或者設置一個類的靜態字段(被final修飾,已在編譯期把結果放入常量池的除外),以及調用一個類的靜態方法的時候。
  2. 對類進行反射調用的時候,如果沒有進行過初始化則需要先出發其初始化過程。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先出發其父類的初始化過程。
  4. 當虛擬機啟動時,定義了入口(含有main()方法的那個類)的主類,虛擬機會先初始化這個主類。
  5. 當使用JDK1.7及以上的版本中的動態語言支持時,若一個java.lang.invoke.MethodHandle實例最后的解析結果是:REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先出發它的初始化過程。

虛擬機規范中指出有且只有這5種場景會出發初始化,並且這5種場景的行為稱為對一個類的“主動引用”,除此之外所有引用類的方式都不會觸發初始化,不觸發初始化的也被稱為被動引用

 用代碼例子來說明被動引用。

/**
 * 通過子類引用父類的的靜態字段,不會導致子類初始化
 */
public class SuperClass {


    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 888;

    public static final String JVM_TEST = "JVM TEST";
}

/**
 * 子類
 */
public class SubClass extends SuperClass {

    static {
        System.out.println("SubClass init!");
    }

}

/**
 * 測試
 */
public class Test {

    public static void main(String[] args){
        System.out.println(SubClass.value);
    }
}

打印結果為:

SuperClass init!
888

對於靜態字段,只有定義這個字段的類才會被初始化,因此通過子類調用其父類中定義的靜態字段,只會出發父類的初始化。

 

/**
 * 通過數組定義引用類,不會出發類的初始化
 */
public class Test {

    public static void main(String[] args){
        SuperClass[] supers = new SuperClass[12];
    }
}

運行結果並沒有打印出“SuperClass init!”,這說明並沒有對SuperClass進行初始化,定義數組不會觸發類的初始化

 

/**
 * 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類中,因此不會出發定義常量的類的初始化。
 */
public class Test {

    public static void main(String[] args){
        System.out.println(SuperClass.JVM_TEST);
    }
}

運行結果也沒有打印出“SuperClass init!”,因為雖然引用了SuperClass的常量,但其實在編譯極端通過常量傳播優化,已經將此常量存儲到了Test類的常量池中,因Test類對此常量的引用,都會轉化為Test類對自身常量池的引用了。這說明SuperClass和Test這兩個類,在編譯階段完成后就沒有任何關系了。

接口的加載過程和類的加載過程步驟上是一致的,但是稍有不同的是上面的例子都是用靜態語句塊“static{}”來輸出初始化信息的,在接口中不能使用“static{}”靜態語句塊。還有一個不同是:當一個類在初始化的時候,要求其父類全部都已經初始化過了,但是一個接口在初始化的時候,不要求其父接口都初始化過,只有真正使用到父接口的時候(例如:引用父接口中定義的常量)才會初始化。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM