1.概述
虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
與那些在編譯時需要進行連接工作的語言不同,在Java語言里面,類型的加載、連接和初始化過程都是在程序運行期間完成的,這種策略雖然會令類加載時稍微增加一些性能開銷,但是會為 Java應用程序提供高度的靈活性,Java里天生可以動態(tài)擴展的語言特性就是依賴運行期動態(tài)加載和動態(tài)連接這個特點實現(xiàn)的。
2.類加載的時機(類的生命周期)
類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和卸載這7個階段。其中,驗證、準備和解析這3個部分統(tǒng)稱為連接。

加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時綁定(也稱為動態(tài)綁定或晚期綁定)。
什么情況下需要開始類加載過程的第一個階段:加載?Java虛擬機規(guī)范并沒有對此作出強制約束。但是對于初始化階段階段,虛擬機規(guī)范則是嚴格規(guī)定了有且只有5種情況必須立即對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):
- 遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實例化對象、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候,以及調(diào)用一個類的靜態(tài)方法的時候。
- 使用java.lang.reflect包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。
- 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則需要先觸發(fā)其父類的初始化。
- 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main方法的那個類),虛擬機會先初始化這個主類。
- 當使用JDK1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發(fā)其初始化。
3.類加載過程
接下來我們將詳細講解一下Java虛擬機中類加載的全過程,也就是加載、驗證、準備、解析和初始化這5個階段所執(zhí)行的具體動作。
3.1.加載
在加載階段,虛擬機需要完成以下3件事情:
- 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流;
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu);
- 在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
注意:相對于類加載過程的其他階段,一個非數(shù)組類的加載階段使開發(fā)人員可控性最強的,因為加載階段既可以使用系統(tǒng)提供的引導類加載器來完成,也可以由用戶自定義的類加載器去完成。對于數(shù)組類而言,情況有所不同,數(shù)組類本身不通過類加載器創(chuàng)建,它由Java虛擬機直接創(chuàng)建。但數(shù)組類與類加載器仍然密切相關(guān),因為數(shù)組類的元素類型最終是要靠類加載器去創(chuàng)建的。一個數(shù)組類創(chuàng)建過程遵循以下的原則:
- 如果數(shù)組的元素類型是引用類型,那就遞歸加載過程去加載這個元素類型,數(shù)組將在加載該元素類型的類加載器的類名稱空間上被標識。
- 如果數(shù)組的元素類型不是引用類型(例如int[]數(shù)組),Java虛擬機將會把數(shù)組標記為與引導類加載器關(guān)聯(lián)。
- 數(shù)組類的可見性與它的元素類型的可見性一致,如果元素類型不是引用類型,那數(shù)組類的可見性將默認為public。
3.2.驗證
驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
驗證階段包括以下4個階段的校驗動作:
- 文件格式驗證:驗證字節(jié)流是否符合Class文件的規(guī)范,并且能被當前版本的虛擬機處理。
- 元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義分析,保證其描述的信息符合Java語言規(guī)范。
- 字節(jié)碼驗證:主要目的是通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的符合邏輯的。
- 符號引用驗證:確保解析動作能正常執(zhí)行,如果無法通過符號引用驗證,那么將會拋出一個java.lang.IncompatibleClassChangeErroe異常的子類,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
3.3.準備
準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中分配。這個階段中有兩個容易產(chǎn)生混淆的概念需要強調(diào)一下,首先,這時候進行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。其次,這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值。假設(shè)一個類變量的定義為:public static int value = 123;那變量value在準備階段過后的初始值為0而不是123,因為這個時候尚未開始執(zhí)行任何Java方法,而把value賦值為123的putstatic指令時程序被編譯后,存放在類構(gòu)造器<clinit>()方法之中,所以把value賦值為123的動作將在初始化階段才會執(zhí)行。
3.4.解析
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程,符號引用在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現(xiàn)。那么在解析階段中所說的直接引用和符號引用又有什么關(guān)聯(lián)呢?
- 符合引用(Symbolic References):符號引用一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān),引用的目標并不一定已經(jīng)加載到內(nèi)存中。
- 直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現(xiàn)的內(nèi)存布局相關(guān)的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經(jīng)在內(nèi)存中存在。
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符7類符號引用進行,分別對應于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info這7種常量類型。
3.5.初始化
類初始化階段是類加載過程的最后一步,前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與外,其余動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼(或者說是字節(jié)碼)。
在準備階段,變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段,則根據(jù)程序員通過程序制定的主管計劃去初始化類變量和其他資源,或者可以從另一個角度來表達:初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。
- <clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在靜態(tài)語句塊之后的變量可以賦值,但是不能訪問。
- <clinit>()方法與類的構(gòu)造函數(shù)<init>()方法不同,它不需要顯示地調(diào)用父類構(gòu)造器,虛擬機會保證在子類的<clinit>()方法執(zhí)行之前,父類的<clinit>()方法已經(jīng)執(zhí)行完畢。因此在虛擬機中第一個被執(zhí)行的<clinit>()方法的類肯定是java.lang.Object。
- 由于父類的<clinit>()方法先執(zhí)行,意味著父類中定義的靜態(tài)語句塊(static{}塊)要優(yōu)先于子類的變量賦值操作。以下代碼中,字段B的值將會是2而不是1。
public class Test {
static class SuperClass{
public static int A =1;
static {
A = 2;
}
}
static class SubClass extends SuperClass{
public static int B = A;
}
public static void main(String[] args) {
System.out.println(SubClass.B);
}
}
- <clinit>()方法對于類或接口來說并不是必須的,如果一個類中沒有靜態(tài)語句塊(static{}塊),也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法。
- 接口中不能只用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口和類一樣都會生成<clinit>()方法。但接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。
- 虛擬機會保證一個類的 <clinit>()方法在多線程環(huán)境中被正確地加鎖、同步,如果多個現(xiàn)場 同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的<clinit>()方法,其他現(xiàn)場都需要阻塞等待,直到活動線程執(zhí)行<clinit>()方法完畢。
4.類加載器
虛擬機設(shè)計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節(jié)流”這個動作放到Java虛擬機外部去實現(xiàn),以便讓應用程序自己決定如何去獲取所需要的類,實現(xiàn)這個動作的代碼模塊稱為“類加載器”。
4.1.類與類加載器
類加載器雖然只用于實現(xiàn)類的家在動作,但它在Java程序中起到的作用卻遠遠不限于類加載階段。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。這句話通俗點表示為:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
4.2.雙親委派模型
從Java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(xiàn),是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由Java語言實現(xiàn),獨立于虛擬機外部,并且都繼承自抽象類java.lang.ClassLoader。
- 啟動類加載器(Bootstrap ClassLoader)
這個類加載器負責將存放在<JAVA_HOME>\lib目錄中或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機識別的類庫加載到虛擬機內(nèi)存中。啟動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器,那直接使用null代替即可。 - 擴展類加載器(Extension ClassLoader)
這個加載器由sun.misc.Launcher$ExtClassLoader實現(xiàn),它負責加載<JAVA_HOME>\lib\ext目錄中的或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫,開發(fā)者可以直接使用擴展類加載器。 - 應用程序類加載器(Application ClassLoader)
這個加載器由sun.misc.Launcher$AppClassLoader實現(xiàn)。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱為系統(tǒng)類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

類加載器雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類夾在其中,只有當父加載器反饋自己無法完成這個加載請求時,子加載器 才會自己嘗試去加載。
4.3.破壞雙親委派模型
雙親委派模型的3次較大規(guī)模的“被破壞”情況:
- 第一次“被破壞”
發(fā)生在雙親委派模型出現(xiàn)之前,即JDK1.2版本發(fā)布之前。為了向前兼容,在java.lang.ClassLoader添加了一個新的protected方法findClass()。 - 第二次“被破壞”
由雙親委派模型自身的缺陷所導致的。如啟動類加載器無法識別JNDI接口提供者(SPI,Service Provide Interface)的代碼。為了解決類似的問題,引進了 線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設(shè)置,如果創(chuàng)建線程時還未設(shè)置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內(nèi)都沒有設(shè)置過的話,那這個類加載器默認就是應用程序類加載器。 - 第三次“被破壞”
由于用戶對程序動態(tài)性的追求而導致的。例如,目前OSGI已成為業(yè)界事實上的Java模塊化標準,而OSGI實現(xiàn)模塊化熱部署的關(guān)鍵則是它自定義的類加載器機制的實現(xiàn)。每一個程序模塊(Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現(xiàn)代碼的熱替換。