1. 程式人生 > 實用技巧 >關於JVM 類載入的一些測試案例及說明

關於JVM 類載入的一些測試案例及說明

類載入的過程

類載入由7個步驟完成,看圖

載入

1、通過類的全限定名獲取儲存該類的class檔案(沒有指明必須從哪獲取)

2、解析成執行時資料,即instanceKlass例項,存放在方法區

3、在堆區生成該類的Class物件,即instanceMirrorKlass例項

    

何時載入

虛擬機器規範中並沒有強制約束何時進行載入,但是規範嚴格規定了有且只有下列五種情況必須對類進行載入(載入、驗證、準備都會隨之發生),稱為主動引用

1、遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有被載入,則需要先載入。
生成這4條指令的最常見的Java程式碼場景是:

  • 使用new關鍵字例項化物件的時候
  • 讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候
  • 呼叫一個類的靜態方法的時候

2、使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有被載入,則需要先載入。

3、當載入一個類的時候,如果發現其父類還沒有被載入,則需要先載入其父類。

4、當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先載入這個主類(當然如果主類存在未載入的父類,會先載入父類)。

5、當使用JDK1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有被載入,則需要先載入。

預載入:

包裝類、String、Thread

因為沒有指明必須從哪獲取class檔案,腦洞大開的工程師們開發了這些

1、從壓縮包中讀取,如jar、war

2、從網路中獲取,如Web Applet

3、動態生成,如動態代理、CGLIB

4、由其他檔案生成,如JSP

5、從資料庫讀取

6、從加密檔案中讀取


接下來直接上一些測試例子及結果說明:

測試程式碼1

public class Test_1 {
    public static void main(String[] args) {
        System.out.println(Test_1_B.str);
        
while (true); } } class Test_1_A{ public static String str = "A str"; static { System.out.println("A Static Block"); } } class Test_1_B extends Test_1_A{ static { System.out.println("B Static Block"); } }

測試結果:

原因分析:

本示例看似滿足載入時機的第一條:當要獲取某一個類的靜態成員變數的時候如果該類尚未載入,則對該類進行載入。但對於靜態欄位,只有直接定義這個欄位的類才會被載入,因此通過其子類來引用父類中定義的靜態欄位屬於間接引用,只會觸發父類的載入而不會觸發子類的載入。

測試程式碼2

public class Test_2 {
    public static void main(String[] args) {
        System.out.printf(Test_2_B.str);
    }
}

class Test_2_A {
    static {
        System.out.println("A Static Block");
    }
}

class Test_2_B extends Test_2_A {
    public static String str = "B str";

    static {
        System.out.println("B Static Block");
    }
}

測試結果:

原因分析:

本示例滿足載入時機的第一條:當要獲取某一個類的靜態成員變數的時候如果該類尚未載入,則對該類進行載入。

本示例滿足載入時機的第三條:當載入一個類的時候,如果發現其父類還沒有被載入,則需要先載入其父類。

但對於靜態欄位,只有直接定義這個欄位的類才會被載入。此示例對於靜態欄位的引用是直接引用,所以會觸發子類的載入。

測試程式碼3

public class Test_3 {

    public static void main(String[] args) {
        System.out.printf(Test_3_B.str);
    }
}

class Test_3_A {
    public static String str = "A str";

    static {
        System.out.println("A Static Block");
    }
}

class Test_3_B extends Test_3_A {
    public static String str = "B str";

    static {
        System.out.println("B Static Block");
    }
}

測試結果:

原因分析:

本示例滿足載入時機的第一條:當要獲取某一個類的靜態成員變數的時候如果該類尚未載入,則對該類進行載入。

本示例滿足載入時機的第三條:當載入一個類的時候,如果發現其父類還沒有被載入,則需要先載入其父類。

但對於靜態欄位,只有直接定義這個欄位的類才會被載入。此示例對於子類的靜態欄位對父類的靜態欄位進行了覆蓋。引用使用是直接引用,所以會觸發子類的載入。在載入子類的時候,會優先載入其父類。

測試程式碼4

public class Test_4 {
    public static void main(String[] args) {
        Test_4[] arrs =new Test_4[1];
    }
}
class Test_4_A{
    static {
        System.out.println("Test_4_A Static Block");
    }
}

測試結果:

原因分析:

本示例滿足載入時機的第四條:當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先載入這個主類(當然如果主類存在未載入的父類,會先載入父類)。

雖然Test_4_A這個類和包含main方法的主類寫在同一個檔案中,但是編譯器編譯後,任然生產兩個對立的class檔案。虛擬機器啟動時,按需載入只會載入Test_4這個類。

測試程式碼5

public class Test_5 {

    public static void main(String[] args) {
        Test_5_A[] obj = new Test_5_A[1];
    }
}

class Test_5_A {
    static {
        System.out.println("Test_5_A Static Block");
    }
}

測試結果:

原因分析:

本示例滿足載入時機的第四條:當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先載入這個主類(當然如果主類存在未載入的父類,會先載入父類)。

這個過程看似也滿足載入時機的第一條:遇到new建立物件時若類沒被載入,則載入該類。
執行之後發現沒有輸出“Test_5_A Static Block”,說明並沒有觸發類cn.jvm.classload.Test_5_A的載入階段。但是這段程式碼裡面觸發了另外一個名為 [Lcom.jvm.classload.Test_5_A 的類的載入階段。對於使用者程式碼來說,這並不是一個合法的類名稱,它是一個由虛擬機器自動生成的、直接繼承於java.lang.Object的子類,建立動作由位元組碼指令anewarray觸發。
這個類代表了一個元素型別為cn.jvm.classload.Test_5_A的一維陣列,陣列中應有的屬性和方法(使用者可直接使用的只有被修飾為public的length屬性和clone()方法)都實現在這個類裡。
簡言之,現在通過new要建立的是一個Test_5_A陣列物件,而非Test_5_A類物件,因此也屬於間接引用,不會載入Test_5_A類。

測試程式碼6

public class Test_6 {

    public static void main(String[] args) {
        Test_6_A obj = new Test_6_A();
    }
}

class Test_6_A {
    static {
        System.out.println("Test_6_A Static Block");
    }
}

測試結果:

原因分析:

本示例滿足載入時機的第一條:使用new關鍵字例項化類物件的時候,如果該類尚未載入,則對該類進行載入。

測試程式碼7

public class Test_7 {
    public static void main(String[] args) {
        System.out.println(Test_7_A.str);
    }
}
class Test_7_A{
    public static final String str = "A Str";

    static {
        System.out.println("Test_7_A Static Block");
    }
}

測試結果:

原因分析:

本示例滿足載入時機的第一條:讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候。 Test_7_A類在編譯時已經將靜態屬性的常量值存入常量池,Test_7在編譯時也直接指向了常量池。所以不需要載入Test_7_A類。見下圖:

測試程式碼8

public class Test_8 {
    public static void main(String[] args) {
        System.out.println(Test_8_A.uuid);
    }
}

class Test_8_A{
    public static final String uuid= UUID.randomUUID().toString();

    static {
        System.out.println("Test_8_A Static Block");
    }
}

測試結果:

原因分析:

本示例看似也滿足載入時機的第一條:讀取或設定一個類的靜態欄位,此屬性被final修飾,應該不載入Test_8_A類。但是結果為什麼會顯示載入了Test_8_A類呢?

這是因為被final修飾的靜態屬性的值不是常量值,無法再編譯時確定值,只有在執行是才能進行讀取。所以需要載入Test_8_A類。詳見下圖:

測試程式碼9

public class Test_9 {
    static {
        System.out.println("Test_9 Static Block");
    }

    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName("com.jvm.classload.Test_1_A");
    }
}

測試結果:

原因分析:

本示例滿足載入時機的第二條:使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有被載入,則需要先載入。

本示例滿足載入時機的第四條:當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先載入這個主類(當然如果主類存在未載入的父類,會先載入父類)。

測試程式碼10

public class Test_10 {
    public static void main(String[] args) {
        System.out.println(new Test_10_B().str);
    }
}

class Test_10_A{
    static {
        System.out.println("A Static Block");
    }
}

class Test_10_B extends Test_10_A{
    public String str="B Str";
    static {
        System.out.println("B Statci Block");
    }
}

測試結果:

原因分析:

本示例滿足載入時機的第一條:使用new關鍵字例項化物件的時候,如果類沒有被載入,則需要先載入。

本示例滿足載入時機的第三條:當載入一個類的時候,如果發現其父類還沒有被載入,則需要先載入其父類。