深入理解Java虛擬機器之類載入機制
虛擬機器類載入機制
JVM類載入機制分為五個部分: 載入,驗證,準備,解析,初始化 ,順序如下

image
在這五個階段中,載入、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是為了支援Java語言的執行時繫結(也成為動態繫結或晚期繫結)。另外注意這裡的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中呼叫或啟用另一個階段。
載入
載入(Loading)階段是 類載入 (Class Loading)過程的第一個階段,在此階段,虛擬機器需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流。
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
- 在Java堆中生成一個代表這個類的java.lang.Class物件,作為方法區這些資料的訪問入口。
載入階段即可以使用系統提供的類載入器在完成,也可以由使用者自定義的類載入器來完成。載入階段與連線階段的部分內容(如一部分位元組碼檔案格式驗證動作)是交叉進行的,載入階段尚未完成,連線階段可能已經開始。
驗證
1. 檔案格式驗證,是要驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理。如驗證魔數是否0xCAFEBABE;主、次版本號是否正在當前虛擬機器處理範圍之內;常量池的常量中是否有不被支援的常量型別……該驗證階段的主要目的是保證輸入的位元組流能正確地解析並存儲於方法區中,經過這個階段的驗證後,位元組流才會進入記憶體的方法區中儲存,所以後面的三個驗證階段都是基於方法區的儲存結構進行的。
2. 元資料驗證,是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求。可能包括的驗證如:這個類是否有父類;這個類的父類是否繼承了不允許被繼承的類;如果這個類不是抽象類,是否實現了其父類或介面中要求實現的所有方法……
3. 位元組碼驗證,主要工作是進行資料流和控制流分析,保證被校驗類的方法在執行時不會做出危害虛擬機器安全的行為。如果一個類方法體的位元組碼沒有通過位元組碼驗證,那肯定是有問題的;但如果一個方法體通過了位元組碼驗證,也不能說明其一定就是安全的。
4. 符號引用驗證,發生在虛擬機器將符號引用轉化為直接引用的時候,這個轉化動作將在“解析階段”中發生。驗證符號引用中通過字串描述的許可權定名是否能找到對應的類;在指定類中是否存在符合方法欄位的描述符及簡單名稱所描述的方法和欄位;符號引用中的類、欄位和方法的訪問性(private、protected、public、default)是否可被當前類訪問
驗證階段對於虛擬機器的類載入機制來說,不一定是必要的階段。如果所執行的全部程式碼確認是安全的,可以使用-Xverify:none引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入時間。
準備
準備階段是為類的靜態變數分配記憶體並將其初始化為 預設值 ,這些記憶體都將在方法區中進行分配。準備階段不分配類中的例項變數的記憶體,例項變數將會在物件例項化時隨著物件一起分配在Java堆中。
public static int value=123;//在準備階段value初始值為0 。在初始化階段才會變為123 。
如果類欄位的欄位屬性表中存在ConstantValue屬性,那在準備階段變數value就會被初始化為ConstantValue屬性所指定的值
//個人理解:類常量分配在常量池 public static final int value=123;//在準備階段value初始值為0 。在初始化階段才會變為123 。
解析
解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。
符號引用(Symbolic Reference):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中。
直接引用(Direct Reference):直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼。直接引用是與虛擬機器實現的記憶體佈局相關的,如果有了直接引用,那麼引用的目標必定已經在記憶體中存在。
初始化
類初始化是類載入過程的最後一步,前面的類載入過程,除了在載入階段使用者應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才真正開始執行類中定義的Java程式程式碼。
初始化階段是執行類構造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的。
何時開始 載入
- 遇到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的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行初始化,則需要先出觸發其初始化。
注意,對於這五種會觸發類進行初始化的場景,虛擬機器規範中使用了一個很強烈的限定語:“有且只有”,這五種場景中的行為稱為對一個類進行 主動引用。除此之外,所有引用類的方式,都不會觸發初始化,稱為 被動引用。
示例1
Parent.class
public class Parent { static { System.out.println("Parent init!!!"); } public static int value = 23; }
Child.class
public class Child extends Parent { static { System.out.println("Child init!!!"); } }
Client.class
public class Client { public static void main(String[] args) { System.out.println(Child.value); } }
結果:
Parent init!!! 23
結論:
對於靜態欄位,只有直接定義這個欄位的類才會被初始化,因此通過其子類來引用父類中定義的靜態欄位,只會觸發父類的初始化而不會出發子類的初始化。
示例2
Client.class
public class Client { public static void main(String[] args) { Parent[] parents = new Parent[10]; } }
結果:
什麼都沒有輸出
結論:
通過陣列定義來引用類,不會觸發類的初始化
示例3
Client.class
public class Client { public static void main(String[] args) { Child child = new Child(); } }
結果:
Parent init!!! Child init!!!
結論:
當初始化一個類時,發現其父類還未初始化,則先出發父類的初始化
示例4
Child2.Class
public class Child2 { static { System.out.println("Child2 init!!!"); } public static final String HELLO_WORLD = "hello world"; }
Client.class
public class Client { public static void main(String[] args) { Child child = new Child(); } }
結果:
hello world
結論:
雖然引用了Child2中的HELLO_WORLD,但其實此常量已經被儲存到了類的常量池中,,並沒有Child類的符號引用
小結:
- 子類呼叫父類的靜態變數,子類不會被初始化。只>有父類被初始化。。對於靜態欄位,只有直接定義這>個欄位的類才會被初始化.
- 通過陣列定義來引用類,不會觸發類的初始化
- 訪問類的常量,不會初始化類
示例5
class SingleTon { private static SingleTon singleTon = new SingleTon(); public static int count1; public static int count2 = 0; private SingleTon() { count1++; count2++; } public static SingleTon getInstance() { return singleTon; } } public class Test { public static void main(String[] args) { SingleTon singleTon = SingleTon.getInstance(); System.out.println("count1=" + singleTon.count1); System.out.println("count2=" + singleTon.count2); } }
**Singleton輸出結果:1 0 **
分析:
- 首先執行main中的Singleton singleton = Singleton.getInstance(); 呼叫靜態方法觸發載入
- 類的載入:載入類Singleton
- 類的驗證
- 類的準備:為靜態變數分配記憶體,設定預設值。這裡為singleton(引用型別)設定為null,value1,value2(基本資料型別)設定預設值0
- 類的初始化(按照賦值語句進行修改):
執行private static Singleton singleton = new Singleton();
執行Singleton的構造器:value1++;value2++; 此時value1,value2均等於1
執行
public static int value1;
public static int value2 = 0;
此時value1=1,value2=0
示例6
class SingleTon { public static int count1; public static int count2 = 0; private static SingleTon singleTon = new SingleTon(); private SingleTon() { count1++; count2++; } public static SingleTon getInstance() { return singleTon; } } public class Test { public static void main(String[] args) { SingleTon singleTon = SingleTon.getInstance(); System.out.println("count1=" + singleTon.count1); System.out.println("count2=" + singleTon.count2); } }
**Singleton輸出結果:1 1 **
分析:
- 首先執行main中的Singleton2 singleton2 = Singleton2.getInstance2();
- 類的載入:載入類Singleton2
- 類的驗證
- 類的準備:為靜態變數分配記憶體,設定預設值。這裡為value1,value2(基本資料型別)設定預設值0,singleton2(引用型別)設定為null,
- 類的初始化(按照賦值語句進行修改):
執行
public static int value2 = 0;
此時value2=0(value1不變,依然是0);
執行
private static Singleton singleton = new Singleton();
執行Singleton2的構造器:value1++;value2++;
此時value1,value2均等於1,即為最後結果
類載入器
虛擬機器設計團隊把載入動作放到JVM外部實現,以便讓應用程式決定如何獲取所需的類,JVM提供了3種類載入器:
- 啟動類載入器(Bootstrap ClassLoader):負責載入 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath引數指定路徑中的,且被虛擬機器認可(按檔名識別,如rt.jar)的類。
- 擴充套件類載入器(Extension ClassLoader):負責載入 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變數指定路徑中的類庫。
- 應用程式類載入器(Application ClassLoader):負責載入使用者路徑(classpath)上的類庫。
JVM通過雙親委派模型進行類的載入,當然我們也可以通過繼承java.lang.ClassLoader實現自定義的類載入器。

image
某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次遞迴,如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去載入。
雙親委派模型的規定通俗來講就是:子載入器載入的類可以使用父載入器載入的類,但是父載入器載入的類不能使用子載入器載入的類。
雙親委派模型很好的解決了各個類載入器載入基礎類的統一性問題。即越基礎的類由越上層的載入器進行載入。
使用雙親委派模型的好處
在於Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係,保證了系統的安全性,防止記憶體中出現多份同樣的位元組碼。
例如類java.lang.Object,它存在在rt.jar中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的Bootstrap ClassLoader進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。相反,如果沒有雙親委派模型而是由各個類載入器自行載入的話,如果使用者編寫了一個java.lang.Object的同名類並放在ClassPath中,那系統中將會出現多個不同的Object類,程式將混亂。因此,如果開發者嘗試編寫一個與rt.jar類庫中重名的Java類,可以正常編譯,但是永遠無法被載入執行,使用自定義類載入器強行載入一個java.開頭的類也是會丟擲SecurityException。