深入理解JVM(③)虛擬機的類加載時機
前言
Java虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這個過程被稱為虛擬機的類加載機制。
類加載的時機
一個類型從被加載到虛擬機內存中開始,到卸載除內存為止,它的生命周期將會經歷加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和 卸載(Unloading)、七個階段,其中驗證、準備、解析三個部分統稱為連接(Linking)。
類的生命周期如下圖:
其實加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,類型的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支持Java語音的運行時綁定特性(也稱為動態綁定或晚期綁定)。
在什麼情況下需要開始類加載過程的第一個階段“加載”,《Java虛擬機規則》中並沒有進行強制約束,但是對於初始化階段《Java虛擬機規範》則是嚴格規定了有且只有以下六種情況必須立即對類進行“初始化”。
- 遇到
new
、getstatic
、putstatic
或invokestatic
這四條字節碼指令時,如果類型沒有進行過初始化,則需要先觸發其初始化階段。
涉及到這四條指令的典型場景有:
- 使用new關鍵字實例化對的時候。
- 讀取或設置一個類型的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候。
- 調用一個類型的靜態方法的時候。
- 使用
java.lang.reflect
包的方法對類型進行反射調用的時候,如果類型沒有進行過初始化,則需要先觸發其初始化。 - 當初始化類型的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
- 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
- 當使用JDK7新加入的動態語言支持時,如果一個
java.lang.invoke.MethodHandle
實例最後的解析結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種類型的方法句柄,並且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化。 - 當一個接口中定義了JDK8新加入的默認方法(被
default
關鍵字修飾的接口方法)時,如果這個接口的實現類發生了初始化,那該接口要在其之前被初始化。
除了以上的這個六種場景外,所有引用類型的方式都不會觸發初始化,稱為被動引用。
下面來看一下哪些是被動引用:
例子1:
父類
package com.jimoer.classloading;
/**
* @author jimoer
* @date Create in 2020/06/24 16:08
* @description 通過子類引用父類的靜態字段,不會導致子類初始化。
*/
public class FatherClass {
static {
System.out.println("FatherClass init!!!!!");
}
public static int value = 666;
}
子類
package com.jimoer.classloading;
public class SonClass extends FatherClass{
static {
System.out.println("SonClass init!!!");
}
}
測試類
@Test
public void testInitClass(){
System.out.println(SonClass.value);
}
運行結果:
FatherClass init!!!!!
666
通過運行結果我們看到,只輸出了“FatherClass init!!!!!”,並沒有輸出“SubClass init!!!”,這是因為對於使用靜態字段,只有直接定義這個字段的類才會被初始化,因此通過子類來引用父類中定義的靜態字段,並不會初始化子類。
例子2:
/**
* 通過數組定義來引用類,不會觸發此類的初始化
*/
@Test
public void testInitClass2(){
FatherClass[] fathers = new FatherClass[5];
}
運行結果:未打印任何信息。
通過運行結果我們發現,並沒有打印出 FatherClass init!!!!! ,這說明並沒有觸發Father類的初始化階段。但是這段代碼裏面觸發了另一個名為“[Lcom.jimoer.classloading.FatherClass
”的類的初始化階段,它是一個由虛擬機自動生成的、直接繼承與java.lang.Object
的子類,創建動作由字節碼newarray觸發。這個類代表了一個元素類型為FatherClass的一維數組,數組中應用的屬性和方法(length屬性和clone()方法)都實現在這個類里。
例子3:
/**
* @author jimoer
* 常量在編譯階段會存入調用類的常量池中,
* 本質上沒有直接引用到定義常量的類,
* 因此不會觸發定義常量的類的初始化。
*/
public class ConstantClass {
static {
System.out.println("ConstantClass init !!!");
}
public static final String CLASS_LOAD = "class load test !!!";
}
使用
/**
* 使用常量
*/
@Test
public void testInitClass3(){
System.out.println(ConstantClass.CLASS_LOAD);
}
運行結果:
class load test !!!
通過運行結果,我們看到當在使用一個類的常量時,並不會初始化定義了常量的類。這是因為雖然在Java源碼中確實引用了ConstatClass
的類的常量CLASS_LOAD
,但其實在編譯階段通過常量傳播優化,已經將此常量的值“class load test !!!
”直接存儲在使用常量的類中的常量池中了,所以在使用ConstantClass.CLASS_LOAD
時候,實際上都被轉化為在使用類自身的常量池的引用了。
接口也是有初始化過程的,上面的代碼都是用靜態語句塊“static {}”來輸出初始化信息的,而接口中不能使用static{}語句塊,但編譯器仍然會為接口生成“ ()”類構造器,用於初始化接口中所定義的成員變量。
還有一點接口與類不同,當一個類在初始化時,要求其父類全部都已經初始化過了,但是在一個接口初始化時,並不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時候(例如引用接口中的常量)才會初始化。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※Google地圖已可更新顯示潭子電動車充電站設置地點!!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!