一篇文章徹底搞懂JVM加載中初始化的時機

JVM類加載過程

JVM類加載過程分爲幾個階段,分別是加載驗證準備解析初始化加載是把二進制字節碼載入內存,驗證是校驗字節流中包含的信息是否符合當要求,準備是爲靜態變量分配內存並設置靜態變量初始值,解析是把常量池內的符號引用替換爲直接引用,初始化是執行所有靜態變量的賦值動作和靜態語句塊中的語句。更多詳盡分析請閱讀之前的文章《JVM的類加載機制全面解析》,這裏不再贅述了。

類初始化的時機

對於我們開發人員,我認爲應該具體瞭解一下初始化階段什麼時候在開始。JVM規範對此做了嚴格規範,有且只有以下5種情況必須對類進行初始化:

  1. 遇到new、getstatic、putstatic或invokestatic這四條字節碼指令時,如果類沒有被初始化過,就需要先進行初始化。對於字節碼指令不瞭解的同學,可能就是一臉蒙圈了。我們來說人話,就是:使用new關鍵字實例化對象的時候、讀取和設置一個類的靜態字段(不被final修飾的)和調用一個類的靜態方法的時候。這樣說更容易被理解一些。

  2. 使用java.lang.reflect包中的方法對類進行反射調用的時候,如果類沒有被初始化過,就需要先進行初始化。

  3. 當初始化一個類的時候,如果發現它的父類還沒有被初始化過,就需要先初始化它的父類。

  4. JVM會先初始化要執行的主類,也是包含main()方法的那個類。

  5. 當使用JDK 1.7的動態語言支持時,如果java.lang.invoke.MethodHandle實例最後的解析結果是REF_getStatic(使用MethodHandle讀取類的靜態字段)、REF_putStatic(使用MethodHandle設置類的靜態字段)、REF_invokeStatic(使用MethodHandle調用類的靜態方法)的方法句柄時,如果這個方法句柄沒有被初始化過,就需要先進行初始化。

被動引用

剛剛提到的5種情況,都會觸發初始化,這些行爲爲稱爲對一個類的主動引用。除了這些以外,所有引用類的方式都不會觸發初始化,被爲被動引用。爲了更好的理解,下面舉幾個被動引用的例子。

通過子類引用父類的靜態變量

public class SuperClass {
    static {
        System.out.println("父類正在初始化");
    }

    public static String name = "萬貓學社";
}
public class SubClass extends SuperClass {
    static {
        System.out.println("子類正在初始化");
    }
}
public class OneMoreStudy {
    public static void main(String[] args) {
        System.out.println(SubClass.name);
    }
}

對於靜態變量,只有直接定義這個變量的類纔會被初始化,通過子類引用父類中定義的靜態變量,只會觸發父類的初始化而不會觸發子類的初始化,運行的結果是:

父類正在初始化
萬貓學社

結果中並沒有“子類正在初始化”。

通過數組定義來引用類

public class OneMoreStudy {
    public static void main(String[] args) {
        SuperClass[] arrays = new SuperClass[10];
        System.out.println("數組元素個數:" + arrays.length);
    }
}

這段代碼中使用之前的SuperClass類,定義了一個SuperClass類的一維數組,運行後的結果是:

數組元素個數:10

結果中並沒有“父類正在初始化”,說明並沒有觸發SuperClass類的初始化。實際上,有一個名爲“[LSuperClass”的類被初始化了,它是由JVM自動生成的、直接繼承於java.lang.Object,創建動作由字節碼指令newarray觸發。

常量

public class ConstClass {
    static {
        System.out.println("有常量的類正在初始化");
    }

    public static final String NAME = "萬貓學社";
}
public class OneMoreStudy {
    public static void main(String[] args) {
        System.out.println(ConstClass.NAME);
    }
}

常量在編譯階段會存入調用類的常量池中,本質沒有直接引用到定義的常量的類,不會觸發定義常量的類的初始化,所以運行的結果是:

萬貓學社

結果中並沒有“有常量的類正在初始化”。

接口初始化的時機

接口也有初始化過程,和類是一致的。不過接口中不能使用“static{}”語句塊,但編譯器仍然會爲接口生成“clinit()”類構造器,用於初始化接口中所定義的成員變量。

接口初始化的時機,基本和之前提到的類的5種情況基本一致,唯一不一樣的是第3種情況:在一個類被初始化時,它的父類也必須被初始化,但是一個接口被初始化時,它的父接口並不要求被初始化。只有在真正使用到父接口時纔會被初始化,比如:引用父接口中定義的常量。

結語

這次主要分享了類在什麼時候被初始化,共有5種情況。除了這種5種情況的引用叫做被動引用,同時舉了3個被動引用的例子。同時,也提到初始化接口和類有什麼不同。

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