虛擬機類加載機制
JVM類加載機制分為五個部分:加載,驗證,準備,解析,初始化,順序如下

在這五個階段中,加載、驗證、準備和初始化這四個階段發(fā)生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之后開始,這是為了支持Java語言的運行時綁定(也成為動態(tài)綁定或晚期綁定)。另外注意這里的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執(zhí)行的過程中調(diào)用或激活另一個階段。
加載
加載(Loading)階段是類加載(Class Loading)過程的第一個階段,在此階段,虛擬機需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
- 在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口。
加載階段即可以使用系統(tǒng)提供的類加載器在完成,也可以由用戶自定義的類加載器來完成。加載階段與連接階段的部分內(nèi)容(如一部分字節(jié)碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經(jīng)開始。
驗證
1. 文件格式驗證,是要驗證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機處理。如驗證魔數(shù)是否0xCAFEBABE;主、次版本號是否正在當(dāng)前虛擬機處理范圍之內(nèi);常量池的常量中是否有不被支持的常量類型……該驗證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲于方法區(qū)中,經(jīng)過這個階段的驗證后,字節(jié)流才會進入內(nèi)存的方法區(qū)中存儲,所以后面的三個驗證階段都是基于方法區(qū)的存儲結(jié)構(gòu)進行的。
2. 元數(shù)據(jù)驗證,是對字節(jié)碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求??赡馨ǖ尿炞C如:這個類是否有父類;這個類的父類是否繼承了不允許被繼承的類;如果這個類不是抽象類,是否實現(xiàn)了其父類或接口中要求實現(xiàn)的所有方法……
3. 字節(jié)碼驗證,主要工作是進行數(shù)據(jù)流和控制流分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為。如果一個類方法體的字節(jié)碼沒有通過字節(jié)碼驗證,那肯定是有問題的;但如果一個方法體通過了字節(jié)碼驗證,也不能說明其一定就是安全的。
4. 符號引用驗證,發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用的時候,這個轉(zhuǎn)化動作將在“解析階段”中發(fā)生。驗證符號引用中通過字符串描述的權(quán)限定名是否能找到對應(yīng)的類;在指定類中是否存在符合方法字段的描述符及簡單名稱所描述的方法和字段;符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可被當(dāng)前類訪問
驗證階段對于虛擬機的類加載機制來說,不一定是必要的階段。如果所運行的全部代碼確認是安全的,可以使用-Xverify:none參數(shù)來關(guān)閉大部分的類驗證措施,以縮短虛擬機類加載時間。
準備
準備階段是為類的靜態(tài)變量分配內(nèi)存并將其初始化為默認值,這些內(nèi)存都將在方法區(qū)中進行分配。準備階段不分配類中的實例變量的內(nèi)存,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。
public static int value=123;//在準備階段value初始值為0 。在初始化階段才會變?yōu)?23 。
如果類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化為ConstantValue屬性所指定的值
//個人理解:類常量分配在常量池
public static final int value=123;//在準備階段value初始值為0 。在初始化階段才會變?yōu)?23 。
解析
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。
符號引用(Symbolic Reference):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān),引用的目標并不一定已經(jīng)加載到內(nèi)存中。
直接引用(Direct Reference):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現(xiàn)的內(nèi)存布局相關(guān)的,如果有了直接引用,那么引用的目標必定已經(jīng)在內(nèi)存中存在。
初始化
類初始化是類加載過程的最后一步,前面的類加載過程,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導(dǎo)和控制。到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼。
初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的。
何時開始 加載
- 遇到new、getstatic、putstatic或invokestatic這四條字節(jié)碼指令(注意,newarray指令觸發(fā)的只是數(shù)組類型本身的初始化,而不會導(dǎo)致其相關(guān)類型的初始化,比如,new String[]只會直接觸發(fā)String[]類的初始化,也就是觸發(fā)對類[Ljava.lang.String的初始化,而直接不會觸發(fā)String類的初始化)時,如果類沒有進行過初始化,則需要先對其進行初始化。生成這四條指令的最常見的Java代碼場景是:
- 使用new關(guān)鍵字實例化對象的時候;
- 讀取或設(shè)置一個類的靜態(tài)字段(被final修飾,已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時候;
- 調(diào)用一個類的靜態(tài)方法的時候。
使用java.lang.reflect包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。
當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則需要先觸發(fā)其父類的初始化。
當(dāng)虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
當(dāng)使用jdk1.7動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進行初始化,則需要先出觸發(fā)其初始化。
注意,對于這五種會觸發(fā)類進行初始化的場景,虛擬機規(guī)范中使用了一個很強烈的限定語:“有且只有”,這五種場景中的行為稱為對一個類進行 主動引用。除此之外,所有引用類的方式,都不會觸發(fā)初始化,稱為 被動引用。
示例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);
}
}
結(jié)果:
Parent init!!!
23
結(jié)論:
對于靜態(tài)字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態(tài)字段,只會觸發(fā)父類的初始化而不會出發(fā)子類的初始化。
示例2
Client.class
public class Client {
public static void main(String[] args) {
Parent[] parents = new Parent[10];
}
}
結(jié)果:
什么都沒有輸出
結(jié)論:
通過數(shù)組定義來引用類,不會觸發(fā)類的初始化
示例3
Client.class
public class Client {
public static void main(String[] args) {
Child child = new Child();
}
}
結(jié)果:
Parent init!!!
Child init!!!
結(jié)論:
當(dāng)初始化一個類時,發(fā)現(xiàn)其父類還未初始化,則先出發(fā)父類的初始化
示例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();
}
}
結(jié)果:
hello world
結(jié)論:
雖然引用了Child2中的HELLO_WORLD,但其實此常量已經(jīng)被儲存到了類的常量池中,,并沒有Child類的符號引用
小結(jié):
- 子類調(diào)用父類的靜態(tài)變量,子類不會被初始化。只>有父類被初始化。。對于靜態(tài)字段,只有直接定義這>個字段的類才會被初始化.
- 通過數(shù)組定義來引用類,不會觸發(fā)類的初始化
- 訪問類的常量,不會初始化類
示例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輸出結(jié)果:1 0 **
分析:
- 首先執(zhí)行main中的Singleton singleton = Singleton.getInstance(); 調(diào)用靜態(tài)方法觸發(fā)加載
- 類的加載:加載類Singleton
- 類的驗證
- 類的準備:為靜態(tài)變量分配內(nèi)存,設(shè)置默認值。這里為singleton(引用類型)設(shè)置為null,value1,value2(基本數(shù)據(jù)類型)設(shè)置默認值0
- 類的初始化(按照賦值語句進行修改):
執(zhí)行private static Singleton singleton = new Singleton();
執(zhí)行Singleton的構(gòu)造器:value1++;value2++; 此時value1,value2均等于1
執(zhí)行
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輸出結(jié)果:1 1 **
分析:
- 首先執(zhí)行main中的Singleton2 singleton2 = Singleton2.getInstance2();
- 類的加載:加載類Singleton2
- 類的驗證
- 類的準備:為靜態(tài)變量分配內(nèi)存,設(shè)置默認值。這里為value1,value2(基本數(shù)據(jù)類型)設(shè)置默認值0,singleton2(引用類型)設(shè)置為null,
- 類的初始化(按照賦值語句進行修改):
執(zhí)行
public static int value2 = 0;
此時value2=0(value1不變,依然是0);
執(zhí)行
private static Singleton singleton = new Singleton();
執(zhí)行Singleton2的構(gòu)造器:value1++;value2++;
此時value1,value2均等于1,即為最后結(jié)果
類加載器
虛擬機設(shè)計團隊把加載動作放到JVM外部實現(xiàn),以便讓應(yīng)用程序決定如何獲取所需的類,JVM提供了3種類加載器:
- 啟動類加載器(Bootstrap ClassLoader):負責(zé)加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數(shù)指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。
- 擴展類加載器(Extension ClassLoader):負責(zé)加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統(tǒng)變量指定路徑中的類庫。
- 應(yīng)用程序類加載器(Application ClassLoader):負責(zé)加載用戶路徑(classpath)上的類庫。
JVM通過雙親委派模型進行類的加載,當(dāng)然我們也可以通過繼承java.lang.ClassLoader實現(xiàn)自定義的類加載器。

某個特定的類加載器在接到加載類的請求時,首先將加載任務(wù)委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務(wù),就成功返回;只有父類加載器無法完成此加載任務(wù)時,才自己去加載。
雙親委派模型的規(guī)定通俗來講就是:子加載器加載的類可以使用父加載器加載的類,但是父加載器加載的類不能使用子加載器加載的類。
雙親委派模型很好的解決了各個類加載器加載基礎(chǔ)類的統(tǒng)一性問題。即越基礎(chǔ)的類由越上層的加載器進行加載。
使用雙親委派模型的好處
在于Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,保證了系統(tǒng)的安全性,防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼。
例如類java.lang.Object,它存在在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的Bootstrap ClassLoader進行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類。相反,如果沒有雙親委派模型而是由各個類加載器自行加載的話,如果用戶編寫了一個java.lang.Object的同名類并放在ClassPath中,那系統(tǒng)中將會出現(xiàn)多個不同的Object類,程序?qū)⒒靵y。因此,如果開發(fā)者嘗試編寫一個與rt.jar類庫中重名的Java類,可以正常編譯,但是永遠無法被加載運行,使用自定義類加載器強行加載一個java.開頭的類也是會拋出SecurityException。