1. 程式人生 > >Java虛擬機器類載入時機

Java虛擬機器類載入時機

文章摘自:深入理解Java虛擬機器 第二版 周志明著 

程式碼編譯的結果從本地機器碼轉變為位元組碼。

在Class檔案中描述的各種資訊,最終都需要載入到虛擬機器中之後才能執行和使用。虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。

類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入Loading、驗證Verification、準備Preparation、解析Resolution、初始化Initialization、使用Using和解除安裝Unloading7

個階段。其中驗證、準備、解析3個部分統稱為連線Linking。

載入、驗證、準備、初始化和解除安裝這5個階段的順序是確定的,類的載入過程必須按照這種順序按部就班地開始,而解析階段不一定。解析階段在某些清空下可以在初始化階段之後再開始,這是為了支援Java語言的執行時繫結

對於初始化階段,虛擬機器規範嚴格規定了有且只有5種情況必須離即對類進行“初始化”(載入驗證準備自然需要在此之前開始):

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

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

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

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

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

有且只有這5中場景中的行為稱為對一個類進行主動引用。除此之外,所有應用類的方式都不會出發初始化,稱為被動引用。如下為被動引用的例子:

/**
 * 被動使用類欄位:
 * 通過子類引用父類的靜態欄位,不會導致子類初始化
 * @author gary
 *
 */
public class SuperClass {
	
	static {
		System.out.println("SuperClass init!");
	}
	
	public static int value = 123;
}

public class SubClass extends SuperClass {
	
	static {
		System.out.println("SubClass init!");
	}
}

public class ConstClass {
	
	static {
		System.out.println("ConstClass init!");
	}
	
	public static final String HELLOWORLD = "hello world";
}

public class NotInitialization {
	
	public static void main(String[] args) {
		System.out.println(SubClass.value);
	}
}

SuperClass init!
123

可以看出,對於靜態欄位,只有直接定義這個欄位的類才會被初始化,因此通過其子類來引用父類中定義的靜態欄位,只會出發父類的初始化而不會出發子類的初始化。

public class NotInitialization {
	
	public static void main(String[] args) {
		//System.out.println(SubClass.value);
		SuperClass[] sca = new SuperClass[10];
	}
}

可以看出,並沒有輸出SuperClass init,證明沒出發SuperClass的初始化階段。但是這段程式碼出發了另外一個類的初始化,這個類代表了一個元素型別為...SuperClass的一維陣列,陣列中應有的屬性和方法都實現在這個類裡。Java語言中對陣列的訪問比C/C++相對安全是因為這個類封裝了陣列元素的訪問方法,而C/C++直接翻譯為對陣列指標的移動。

public class NotInitialization {
	
	public static void main(String[] args) {
		//System.out.println(SubClass.value);
		//SuperClass[] sca = new SuperClass[10];
		System.out.println(ConstClass.HELLOWORLD);
	}
}

hello world

可以看出,沒有輸出ConstClass init,因為雖然在程式碼中引用了ConstCalss類中的常量HELLOWORLD,但其實在編譯階段通過常量傳播優化,已經將此常量的值“hello world”儲存到了NotInitialication類的常量池中,以後NotInitialication對常量ConstClass.HELLOWORLD的引用實際都轉化為NotInitialication類對自身常量池的引用了。

介面也有初始化過程,這點與類一樣。上述程式碼使用靜態語句塊static{}來輸出初始化資訊的,而介面中不能使用static{}語句塊,但是編譯器仍然會為介面生成<clinit>()類構造器,用與初始化介面中所定義的成員變數。介面與類真正有所區別的是前面講述的初始化場景中的第3中:當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個介面初始化時,並不要求其父介面全部都完成初始化,只有在真正使用父接口才回初始化。