類的載入時機

江南入直發表於2021-12-12

  最近在學習java虛擬機器方面的東西,看的是周志明的《深入理解java虛擬機器》,看到類的載入尤其是類的載入時機這一塊覺得受益匪淺,遂記錄一下。

必須初始化的四種情況

有四種情況類是必須要進行初始化的,對於這四種情況原文描述如下:

但是對於初始化階段,虛擬機器規範則是嚴格規定了有且只有4種情況必須立即對類進行初始化,而載入、驗證、準備自然需要在此之前開始。

1:遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令最常見的java程式碼場景是:使用new關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候。

2:使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

3:當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

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

以上四點我們一一用程式碼來驗證,第一點裡面說到了四種初始化的場景,分別是:

①用new關鍵字例項化物件

②讀取類靜態欄位

③設定類的靜態欄位

④呼叫一個類的靜態方法

在驗證之前需要達成一個共識:虛擬機器在初始化類時會執行static語句塊中的操作,因此我們可以根據靜態語句塊中的程式碼是否執行了來判斷類是否載入。為此我建立了一個SubClass類

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class SubClass {
    static{
        System.out.println("子類初始化");
    }
    public static int a = 10;

    public static int getA(){
        return a;
    }
}

在main方法中分別執行(每次執行一條)以下四條程式碼來模擬上面四個場景

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    public static void main(String[] args) {
        SubClass subClass = new SubClass();
        System.out.println(SubClass.a);
        SubClass.getA();
        SubClass.a = 30;
    }
}

結果不出所料,輸出結果都包含"子類初始化",說明以上四種方式確實可以會觸發類的初始化。

接下來看第二點,對類進行反射呼叫時會觸發類的初始化

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("com.test.jvm.classloading.SubClass");
    }
}

以上的反射呼叫同樣正常輸出了"子類初始化"。

第三點如果父類沒有進行初始化,則要先觸發父類的初始化,再建立一個父類,並且讓之前的子類繼承父類

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class SuperClass {
    static {
        System.out.println("父類初始化");
    }
    public static int b = 20;
}
package com.test.jvm.classloading;

/**
 * @author fc
 */
public class SubClass extends SuperClass {
    static{
        System.out.println("子類初始化");
    }
    public static int a = 10;

    public static int getA(){
        return a;
    }
}

這時我們再次執行上面的main方法裡面的任意一條測試語句,這時發現在原來的輸出"子類初始化"前輸出了"父類初始化",說明了兩點:①父類同樣會初始化;②父類會先於子類初始化。

第四點虛擬機器會先初始化包含main方法的主類,這時我們在主類中加入靜態程式碼塊

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    static {
        System.out.println("初始化主類");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        SubClass subClass = new SubClass();
    }
}

可以看到輸出結果如下,完全印證了第四點。

 

不主動進行初始化 

而對於不會主動進行初始化的情況在該書中也有以下幾種情況

第一種是通過子類類名呼叫父類靜態程式碼(包括靜態方法和靜態變數)不會進行初始化,以下也通過程式碼進行說明

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(SubClass.b);
    }
}

輸出如下,可以看到只初始化了父類而沒有初始化子類。

 第二種是通過陣列來建立物件不會觸發此類的初始化

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        SuperClass[] supers = new SuperClass[10];
    }
}

輸出為空。

 第三種是呼叫final修飾的常量不會觸發類的初始化,為此我在父類中加了一個常量

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class SuperClass {
    static {
        System.out.println("父類初始化");
    }
    public static int b = 20;

    public final static String STATE = "常量";
}
package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    public static void main(String[] args) {
        System.out.println(SuperClass.STATE);
    }
}

可以看到輸出結果只是列印了常量的值,並沒有初始化這個類。

補充

 到這裡對於書中描述的類的載入時機都已經用例子說明了,接下來展示一個在博主 Boblim那看到的一個例子

/**
 * @author fc
 */
class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;

    private SingleTon() {
        count1++;
        count2++;
    }

    public static SingleTon getInstance() {
        return singleTon;
    }
}

public class Test {
    public static void main(String[] args) {
        SingleTon.getInstance();
        System.out.println("count1=" + SingleTon.count1);
        System.out.println("count2=" + SingleTon.count2);
    }
}

輸出count1=1,count2=0,關於為什麼會輸出這個結果在那篇連結的部落格已經做了詳細的說明,同時這個輸出結果也很好地佐證了下面這句話

類構造器<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static塊)中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序所決定的。

正是給類變數賦值時是按照順序進行的,所以上面count2又會被重新賦值為0,才導致這個輸出結果。

相關文章