前言
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{}語句塊,但編譯器仍然會為介面生成“
還有一點介面與類不同,當一個類在初始化時,要求其父類全部都已經初始化過了,但是在一個介面初始化時,並不要求其父介面全部都完成了初始化,只有在真正使用到父介面的時候(例如引用介面中的常量)才會初始化。