深入理解 JVM(1)虛擬機類加載機制

轉載請註明原創出處,謝謝!

HappyFeet的博客

虛擬機如何加載 Class 文件?Class 文件中的信息進入到虛擬機後會發生什麼變化?這些是本文要討論的內容。


1、什麼是虛擬機的類加載機制?

虛擬機把描述類的數據從 Class 文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型。這就是虛擬機的類加載機制(通俗一點講就是把 Class 轉成虛擬機可以使用的 Java 類型,轉換過程中會有校驗、解析和初始化的操作)。

2、類的生命週期:

類從被加載到虛擬機內存開始,到卸載出內存爲止,它的生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段。其中,驗證、準備、解析三個部分統稱爲連接(Linking)。如下圖所示:

類的生命週期

圖中需要注意的地方:加載、驗證、準備、初始化和卸載這五個階段具有確定的順序。這個確定的順序是指"開始",即驗證階段必須等加載開始之後才能開始,準備階段開始必須在驗證開始之後。而 “進行” 和 “完成” 的順序則不一定,這些階段通常都是可以互相交叉地混合式進行的,通常會在一個階段執行的過程中調用、激活另外一個階段。

3、觸發類初始化的條件(有且只有五種情況):

  • new 一個對象的時候、讀取或設置一個類的靜態字段(放入常量池的靜態字段除外:被 final 修飾的 static 變量)的時候,以及調用一個類的靜態方法的時候。

  • 反射調用的時候。

  • 子類初始化觸發父類初始化。

  • 執行的主類(包含 main() 方法的那個類)。

  • 當使用 JDK 1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果是 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄的時候。

下面幾個例子是類的被動引用,不會觸發類的初始化:

(1)通過子類引用父類的靜態字段,不會導致子類初始化

public class SuperClass {

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

    public static int value = 123;

}

public class SubClass extends SuperClass {

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

}

public class NotInitialization {

    public static void main(String[] args) {
        System.out.println(SubClass.value);//通過子類引用父類的靜態字段,不會導致子類初始化
    }
}

(2)通過數組定義來引用類,不會觸發此類的初始化

public class NotInitialization {

    public static void main(String[] args) {
        SuperClass[] superClassArray = new SuperClass[10]; // 通過數組定義來引用類,不會觸發此類的初始化
    }
    // 這段代碼不會輸出 "SuperClass init!",即沒有觸發 SuperClass 的初始化
}

(3)常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化

public class ConstClass {

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

    public static final String HELLO_WORLD = "hello world";

}

public class NotInitialization {

    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO_WORLD); // 引用常量池的變量不會觸發定義常量的類的初始化
    }
    // 不會輸出 "ConstClass init!"
}

4、類加載的過程

  • 加載

    加載階段,虛擬機通過一個類的全限定名來獲取定義此類的二進制流,然後把二進制流的靜態存儲結構轉化爲方法區的運行時數據結構,最後在內存中生成一個對象,作爲方法區這個類各種數據的訪問入口。

  • 驗證(非常重要但非必要)

驗證的目的在於確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。(假如自己編寫的及第三方庫都已經被反覆使用和驗證過,那麼,可以考慮使用 -Xverify:none 參數關閉大部分的類驗證機制,以縮短虛擬機類加載的時間)

驗證階段大致會完成以下四個驗證動作:

(1)文件格式驗證

(2)元數據驗證

(3)字節碼驗證

(4)符號引用驗證

  • 準備

    準備階段是給類成員變量設置初始值(數據類型的零值)。

    這裏的類成員變量指被 static 修飾的變量;初始值指數據類型的零值;而常量(被 static final 修飾的變量)會初始化爲具體的值。

    例如:

    public static final int value = 123;
    // 虛擬機在準備階段就會根據 ConstantValue 的設置將 value 賦值爲 123。
    

    數據類型的零值:

    數據類型的零值

  • 解析

    解析階段主要做的事情是虛擬機將常量池內的符號引用替換成直接引用。
    Java 中編譯時 java.lang.NoSuchFieldErrorjava.lang.NoSuchMethodErrorjava.lang.IllegalAccessError 等異常就是在解析階段拋出的。

  • 初始化

  • 初始化階段是類加載過程的最後一步。前面我們提到,準備階段會給類變量設置初始零值,而在初始化階段,則是給類變量執行代碼裏真正賦值的時候。

    • 賦值操作由所有類變量的賦值動作和靜態語句塊中的語句共同產生,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊可以賦值,但是不能訪問,例如:
    public class Test {
    
        static {
            i = 0; // 靜態語句塊可以給後面定義的類變量賦值,但是不能訪問(編譯器會報 "非法向前引用" )
            // System.out.println(i); Illegal forward reference.
        }
    
        static int i = 1;
    
    }
    

    初始化階段的注意事項:

    (1)靜態成員變量初始化在靜態方法之前( static {} 中都是變量賦值);

    (2)父類初始化在子類之前

    (3)執行接口的初始化的時候不需要先執行父接口的初始化,只有當父接口中定義的變量使用時,父接口才會初始化;

    (4)虛擬機會保證一個類的初始化方法在多線程環境中被正確地加鎖、同步:保證了在同一個類加載器下,一個類型只會初始化一次。

參考資料:

(1)《深入理解 Java 虛擬機》周志明 著.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章