jvm類載入過程_JVM類生命週期概述:載入時機與載入過程
技術標籤:jvm類載入過程
寫在前面
本文概述了JVM載入類的時機和生命週期,並結合典型案例重點介紹了類的初始化過程,揭開了JVM類載入機制的神祕面紗。
JVM類載入機制主要包括兩個問題:類載入的時機與步驟 和 類載入的方式
一個Java物件的建立過程往往包括兩個階段:類初始化階段 和 類例項化階段
注意,本文內容是以HotSpot虛擬機器為基準的。
類載入機制概述
我們知道,一個.java檔案在編譯後會形成相應的一個或多個Class檔案(若一個類中含有內部類,則編譯後會產生多個Class檔案),但這些Class檔案中描述的各種資訊,最終都需要載入到虛擬機器中之後才能被執行和使用。事實上,虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗,轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別的過程就是虛擬機器的類載入機制
類載入的時機
Java類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 解除安裝(Unloading)七個階段。其中準備、驗證、解析3個部分統稱為連線(Linking),如圖所示:
類載入時機
什麼情況下虛擬機器需要開始載入一個類呢?虛擬機器規範中並沒有對此進行強制約束,這點可以交給虛擬機器的具體實現來自由把握。
類初始化時機
1)遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令(注意,newarray指令觸發的只是陣列型別本身的初始化,而不會導致其相關型別的初始化,比如,new String[]只會直接觸發String[]類的初始化,也就是觸發對類[Ljava.lang.String的初始化,而直接不會觸發String類的初始化)時,如果類沒有進行過初始化,則需要先對其進行初始化。生成這四條指令的最常見的Java程式碼場景是:
- 使用new關鍵字例項化物件的時候;
- 讀取或設定一個類的靜態欄位(被final修飾,已在編譯器把結果放入常量池的靜態欄位除外)的時候;
- 呼叫一個類的靜態方法的時候。
- 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
- 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類。
- 當使用jdk1.7動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行初始化,則需要先出觸發其初始化
被動引用的幾種經典場景
通過子類引用父類的靜態欄位,不會導致子類初始化
public class SSClass {
static {
System.out.println( "SSClass" );
}
}
public class SClass extends SSClass {
static {
System.out.println( "SClass init!" );
}
public static int value = 123;
public SClass()
{
System.out.println( "init SClass" );
}
}
public class SubClass extends SClass {
static {
System.out.println( "SubClass init" );
}
static int a;
public SubClass()
{
System.out.println( "init SubClass" );
}
}
public class NotInitialization {
public static void main( String[] args )
{
System.out.println( SubClass.value );
}
}
通過陣列定義來引用類,不會觸發此類的初始化
public class NotInitialization{
public static void main(String[] args){
SClass[] sca = new SClass[10];
}
}
常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
public class ConstClass{
static{
System.out.println("ConstClass init!");
}
public static final String CONSTANT = "hello world";
}
public class NotInitialization{
public static void main(String[] args){
System.out.println(ConstClass.CONSTANT);
}
}
類載入過程
載入(Loading)
在載入階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機器需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流(並沒有指明要從一個Class檔案中獲取,可以從其他渠道,譬如:網路、動態生成、資料庫等);
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;
- 在記憶體中(對於HotSpot虛擬就而言就是方法區)生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口;
驗證(Verification)
- 檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範(例如,是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機器的處理範圍之內、常量池中的常量是否有不被支援的型別)
- 元資料驗證:對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求(例如:這個類是否有父類,除了java.lang.Object之外);
- 位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的;
- 符號引用驗證:確保解析動作能正確執行。
驗證階段是非常重要的,但不是必須的,它對程式執行期沒有影響。如果所引用的類經過反覆驗證,那麼可以考慮採用-Xverifynone引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間。
準備(Preparation)
準備階段是正式為類變數(static 成員變數)分配記憶體並設定類變數初始值(零值)的階段,這些變數所使用的記憶體都將在方法區中進行分配
解析(Resolution)
解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行。
初始化(Initialization)
類初始化階段是類載入過程的最後一步。在前面的類載入過程中,除了在載入階段使用者應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才真正開始執行類中定義的java程式程式碼(位元組碼)。
虛擬機器會保證一個類的類構造器<clinit>()在多執行緒環境中被正確的加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的類構造器<clinit>(),其他執行緒都需要阻塞等待,直到活動執行緒執行<clinit>()方法完畢
在同一個類載入器下,一個型別只會被初始化一次