1. 程式人生 > >全面解析JVM載入中初始化的時機

全面解析JVM載入中初始化的時機

JVM類載入過程

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

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

類初始化的時機

對於我們開發人員,我認為應該具體瞭解一下初始化階段什麼時候在開始。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呼叫類的靜態方法)的方法控制代碼時,如果這個方法控制代碼沒有被初始化過,就需要先進行初始化。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

被動引用

剛剛提到的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);
    }
}

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

父類正在初始化
萬貓學社

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

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

通過陣列定義來引用類

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觸發。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

常量

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);
    }
}

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

萬貓學社

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

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

介面初始化的時機

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

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

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

結語

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

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨