代碼編譯的結(jié)果從本地機(jī)器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲(chǔ)格式發(fā)展的一小步,確實(shí)編譯語(yǔ)言發(fā)展的一大步。
虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的類加載機(jī)制。
與那些編譯時(shí)需要進(jìn)行連接工作的語(yǔ)言不同,在Java語(yǔ)言里,類型的加載、連接和初始化過(guò)程都是在程序運(yùn)行期間完成的,這種策略雖然會(huì)令類加載時(shí)稍微增加一些性能開銷,但是會(huì)為Java應(yīng)用程序提供高度的靈活性,Java里天生可以動(dòng)態(tài)擴(kuò)展的語(yǔ)言特性就是依賴運(yùn)行期間動(dòng)態(tài)加載和動(dòng)態(tài)連接這個(gè)特點(diǎn)實(shí)現(xiàn)的。
為了避免語(yǔ)言表達(dá)可能產(chǎn)生的偏差,筆者先設(shè)定兩個(gè)語(yǔ)言上的約定:
- 第一,在實(shí)際情況中,每個(gè)Class文件都有可能代表著Java語(yǔ)言中的一個(gè)類或者接口,后文直接對(duì)“類”的描述都包括了類和接口的可能性,而對(duì)類和接口需要分開描述的場(chǎng)景會(huì)特別指明;
- 第二,這里所指的“Class文件”是指的一串二進(jìn)制字節(jié)流,無(wú)論以何種形式存在都可以。
類加載的時(shí)機(jī)
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用、卸載這7個(gè)階段。其中驗(yàn)證、準(zhǔn)備、解析3個(gè)部分統(tǒng)稱為鏈接。
加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這5個(gè)階段的順序是確定的,類的加載過(guò)程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某種情況下可以在初始化階段之后再開始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定也稱為動(dòng)態(tài)綁定或晚期綁定。注意:這里說(shuō)的是按部就班的開始,而不是按部就班的完成,強(qiáng)調(diào)這點(diǎn)是因?yàn)檫@些階段通常是相互交叉的混合式進(jìn)行的,通常會(huì)再一個(gè)階段執(zhí)行的過(guò)程中調(diào)用、激活另一個(gè)階段。
Java虛擬機(jī)規(guī)范中并沒(méi)有進(jìn)行強(qiáng)制約束什么情況下開始“加載”,這點(diǎn)可以交給虛擬機(jī)的具體實(shí)現(xiàn)來(lái)自由把握。但是出于初始化階段,虛擬機(jī)規(guī)范中則是嚴(yán)格規(guī)定了有且只有5種情況必須立即對(duì)類進(jìn)行“初始化”(而加載、驗(yàn)證、準(zhǔn)備自然需要在此之前開始):
- 遇到new、getstatic、pubstatic或invokestatic這4條字節(jié)碼指令時(shí),如果類沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場(chǎng)景是:使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候、讀取或設(shè)置一個(gè)類的靜態(tài)字段(被final修飾,已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候,以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候。
- 使用java.lang.refect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。
- 當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其父類的初始化。
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的那個(gè)類),虛擬機(jī)會(huì)初始化這個(gè)主類。
- 當(dāng)使用JDK1.7的動(dòng)態(tài)語(yǔ)言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其初始化。
對(duì)于這5種會(huì)觸發(fā)類進(jìn)行初始化的場(chǎng)景,虛擬機(jī)規(guī)范中使用了一個(gè)很強(qiáng)烈的限定語(yǔ):有且只有,這5種場(chǎng)景中的行為稱為對(duì)一個(gè)類進(jìn)行主動(dòng)引用。除此之外,所有引用類的方式都不會(huì)觸發(fā)初始化,稱為被動(dòng)引用。
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 static void main(String[] args) {
System.out.println(SubClass.value);
}
上面代碼會(huì)輸出“SuperClass init"”而不是“SubClass init”,對(duì)于靜態(tài)字段,只有直接定義這個(gè)字段的類才能初始化,因此通過(guò)子類來(lái)引用父類定義的靜態(tài)字段,會(huì)觸發(fā)父類的初始化而不是觸發(fā)子類的初始化。至于是否要觸發(fā)子類的加載和驗(yàn)證,在虛擬機(jī)規(guī)范中并未明確規(guī)定,這點(diǎn)取決于虛擬機(jī)的具體實(shí)現(xiàn)。對(duì)于HotSpot虛擬機(jī)來(lái)說(shuō),可以通過(guò)-XX:+TraceClassLoading參數(shù)觀察到此操作會(huì)導(dǎo)致子類的加載。
public static void main(String[] args) {
SuperClass[] sc = new SuperClass[10];
}
上面代碼運(yùn)行之后發(fā)現(xiàn)沒(méi)有輸出“SuperClass init”,說(shuō)明并沒(méi)有觸發(fā)類org.fenixsoft.classloading.SuperClass的初始化階段。但是這段代碼里面觸發(fā)了另外一個(gè)名為“Lorg.fenixsoft.classloading.SuperClass”的類的初始化階段,對(duì)于用戶代碼來(lái)說(shuō),這并不是一個(gè)合法的類名稱,它是一個(gè)由虛擬機(jī)自動(dòng)生成的、直接繼承于java.lang.Object的子類,創(chuàng)建動(dòng)作由字節(jié)碼指令newarray觸發(fā)。
這個(gè)類代表了一個(gè)元素類型為org.fenixsoft.classloading.SuperClass的一維數(shù)組,數(shù)組中應(yīng)有的屬性和方法,用戶可直接使用的只有被修飾為public的length屬性和clone()方法)都實(shí)現(xiàn)在這個(gè)類里。Java語(yǔ)言中對(duì)數(shù)組的訪問(wèn)比C/C++ 相對(duì)安全是因?yàn)檫@個(gè)類封裝了數(shù)組元素的訪問(wèn)方法,而C/C++ 直接翻譯為對(duì)數(shù)組指針的移動(dòng)。在Java中,當(dāng)檢查到發(fā)生數(shù)組越界就會(huì)跑出java.lang.ArrayIndexOutOfBoundsExecption異常。
public class ConstClass extends SuperClass {
static {
System.out.println("ConstClass init");
}
public final static int value = 123;
}
public static void main(String[] args) {
System.out.println(ConstClass.value);
}
上述代碼運(yùn)行之后,也沒(méi)有輸出,這是因?yàn)殡m然在Java源碼中引用ConstClass類中的常量,但其實(shí)在編譯階段通過(guò)常量傳播優(yōu)化,已經(jīng)將此常量存儲(chǔ)到了主類的常量池中,以后主類對(duì)常量的引用實(shí)際上被轉(zhuǎn)化為主類對(duì)自身常量池的引用了。也就是說(shuō),實(shí)際上主類的Class文件之中并沒(méi)有ConstClass類的符號(hào)引用入口,這兩個(gè)類在編譯成Class之后就不存在任何聯(lián)系了。
接口的加載過(guò)程與類加載過(guò)程稍有一些不同,針對(duì)接口需要做一些特殊說(shuō)明:接口也有初始化過(guò)程,這點(diǎn)與類是一致的,上面的代碼都是用靜態(tài)語(yǔ)句塊“static{}”來(lái)輸出初始化信息的,而接口中不能使用“static{}”語(yǔ)句塊,但編譯器仍然會(huì)為接口生成“<client>()”類構(gòu)造器,用于初始化接口中所定義的成員變量。接口與類真正有所區(qū)別的是前面講述的5種“有且僅有”需要開始初始化場(chǎng)景中的第三種:當(dāng)一個(gè)類在初始化時(shí),要求其父類全部都已經(jīng)初始化過(guò)了,但是一個(gè)接口在初始化時(shí),并不需要其父接口全部都完成初始化,只有在真正使用到父接口的時(shí)候才會(huì)初始化。
類加載的過(guò)程
Java虛擬機(jī)中類加載的券過(guò)程包括:加載、驗(yàn)證、準(zhǔn)備、解析和初始化5個(gè)階段
加載
“加載”是“類加載”(Class Loading)過(guò)程的一個(gè)階段。在加載階段,虛擬機(jī)需要完成以下3件事情:
- 通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口
虛擬機(jī)規(guī)范的這3點(diǎn)要求其實(shí)并不算具體,因此虛擬機(jī)實(shí)現(xiàn)與具體應(yīng)用的靈活度都是相當(dāng)大的。例如“通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流”這條,它沒(méi)有指明二進(jìn)制字節(jié)流要從一個(gè)Class文件中獲取,準(zhǔn)確的說(shuō)是根本沒(méi)有指明要從哪里獲取、怎樣獲取。虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)在加載階段搭建了一個(gè)相當(dāng)開放的、廣闊的舞臺(tái),Java發(fā)展歷程中,充滿創(chuàng)造力的開發(fā)人員則在這個(gè)舞臺(tái)上,玩出了各種花樣,許多舉足輕重的Java技術(shù)都簡(jiǎn)歷在這個(gè)基礎(chǔ)知識(shí),例如:
- 從ZIP包中讀取,折痕常見,最終成為日后的JAR、EAR、WAR格式的基礎(chǔ)
- 從網(wǎng)絡(luò)中獲取,這種場(chǎng)景最典型的應(yīng)用就是Applet
- 運(yùn)行時(shí)計(jì)算生成,這種場(chǎng)景使用得最多的就是動(dòng)態(tài)代理技術(shù),在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass來(lái)為特定接口生成形式為“*.Proxy”的代理類的二進(jìn)制字節(jié)流。
- 由其他文件生成,典型場(chǎng)景是JSP應(yīng)用,即由JSP文件生成對(duì)應(yīng)的Class類。
- 從數(shù)據(jù)庫(kù)讀取,這種場(chǎng)景相對(duì)少見些,例如有些中間件服務(wù)器(SAP Netweaver)可以選擇把程序安裝到數(shù)據(jù)庫(kù)中來(lái)完成程序代碼在集群間的分發(fā)。
相對(duì)于類加載過(guò)程的其他階段,一個(gè)非數(shù)組類的加載階段(準(zhǔn)確的說(shuō),是加載階段中獲取類的二進(jìn)制字節(jié)流的動(dòng)作)是開發(fā)人員可控性最強(qiáng)的,因?yàn)榧虞d階段既可以使用系統(tǒng)提供的引導(dǎo)類加載器來(lái)完成,也可以由用戶自定義的類加載器去完成,開發(fā)人員可以通過(guò)定義自己的類夾雜去去控制字節(jié)流的獲取方式,即重寫一個(gè)類加載器的loadClass()方法。
對(duì)于數(shù)組類而言,情況就有所不同,數(shù)組類本身不通過(guò)類加載器創(chuàng)建,它是由Java虛擬機(jī)直接創(chuàng)建的。但是數(shù)組類與加載器仍然有很密切的關(guān)系,因?yàn)閿?shù)組類的元素類型(指的是數(shù)組去掉所有維度的類型)最終是靠類加載器去創(chuàng)建,一個(gè)數(shù)組類創(chuàng)建的過(guò)程遵循以下規(guī)則:
- 如果數(shù)組的組件類型是引用類型,數(shù)組將在加載該組件類型的加載器的類名稱空間上被標(biāo)識(shí)。
- 如果數(shù)組的組件類型不是引用類型,例如int[]數(shù)組,JVM會(huì)將數(shù)組標(biāo)記為與引導(dǎo)類加載器關(guān)聯(lián)。
- 數(shù)組類的可見性與它的組件類型的可見性一致,如果組件類型不是引用類型,那數(shù)組類的可見性將默認(rèn)為public
加載階段完成之后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式的存儲(chǔ)格式由虛擬機(jī)實(shí)現(xiàn)自行定義,虛擬機(jī)規(guī)范未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)。然后在內(nèi)存中實(shí)例化一個(gè)java.lang.Class類的對(duì)象(并沒(méi)有明確規(guī)定是在Java堆中,對(duì)于HotSpot虛擬機(jī)而言,Class對(duì)象比較特殊,它雖然是對(duì)象,但是存放在方法區(qū)里),這個(gè)對(duì)象將作為程序訪問(wèn)方法區(qū)中的這些類型數(shù)據(jù)的外部接口。
加載階段與連接階段的部分內(nèi)容(如一部分字節(jié)碼文件格式驗(yàn)證動(dòng)作)是交叉進(jìn)行的,加載階段尚未完成,連接階段可能已經(jīng)開始,但這些夾在加載階段之中進(jìn)行的動(dòng)作,仍然屬于連接階段的內(nèi)容,這兩個(gè)階段的開始時(shí)間仍然保持著固定的先后順序。
驗(yàn)證
驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。
Java語(yǔ)言相對(duì)于C和C++來(lái)說(shuō),本身是相對(duì)安全的語(yǔ)言,使用純粹的Java代碼無(wú)法做到諸如訪問(wèn)數(shù)組便捷以外的數(shù)據(jù)、將一個(gè)對(duì)象轉(zhuǎn)型為它并未實(shí)現(xiàn)的類型、跳轉(zhuǎn)到不存在的代碼行之類的事情,如果這樣做了,編譯器將拒絕編譯。但是Class文件不一定是Java源碼編譯出來(lái),可以使用任何途徑產(chǎn)生,甚至包括用十六進(jìn)制編輯器直接編寫來(lái)產(chǎn)生Class文件。在字節(jié)碼語(yǔ)言層面上,上述Java代碼無(wú)法做到的實(shí)際情況都是可以實(shí)現(xiàn)的,至少在語(yǔ)義上是可以表達(dá)出來(lái)的。虛擬機(jī)如果不檢查輸入的字節(jié)流,對(duì)其完全信任的話,很可能會(huì)因?yàn)檩d入了有害的字節(jié)流而導(dǎo)致系統(tǒng)崩潰,所以驗(yàn)證是虛擬機(jī)對(duì)自身保護(hù)的一項(xiàng)重要工作。
從整體上看,驗(yàn)證階段大致分為4個(gè)階段性的驗(yàn)證動(dòng)作:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證、符號(hào)引用驗(yàn)證。
文件格式驗(yàn)證
第一階段要驗(yàn)證字節(jié)流是否符合Class文件格式規(guī)范,并且當(dāng)能被當(dāng)前版本的虛擬機(jī)處理。這一階段可能包括以下這些驗(yàn)證點(diǎn):
- 是否以魔數(shù)0xCAFEBABE開頭
- 主、次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)
- 常量池的常量是否有不被支持的常量類型
- 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數(shù)據(jù)
- Class文件中各個(gè)部分以及文件本身是否被剎車農(nóng)戶的或附加的其他信息
- ......
實(shí)際上,第一階段的驗(yàn)證點(diǎn)還遠(yuǎn)不止這些,上面這些知識(shí)一小部分內(nèi)容,該驗(yàn)證階段的主要目的是保證輸入的字節(jié)流能正確的加爾西并存儲(chǔ)于方法區(qū)之內(nèi),格式上符合描述一個(gè)Java類型信息的要求。這階段的驗(yàn)證是基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過(guò)了這個(gè)階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),所以后面3個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不會(huì)再直接操作字節(jié)流。
元數(shù)據(jù)驗(yàn)證
第二階段是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證描述的信息符合Java語(yǔ)言規(guī)范的要求,這個(gè)階段的驗(yàn)證如下:
- 這個(gè)類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)當(dāng)有父類)
- 這個(gè)類的父類是否繼承了不被允許繼承的類(被final修飾的類)
- 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口之中要求實(shí)現(xiàn)的所有方法
- 類中的字段、方法是否與父類產(chǎn)生矛盾(例如覆蓋了父類的final字段,或者出現(xiàn)不符合規(guī)則的方法重載等)
- ......
這一階段主要目的是對(duì)類的元數(shù)據(jù)信息進(jìn)行語(yǔ)義校驗(yàn),保證不存在不符合Java語(yǔ)言規(guī)范的元數(shù)據(jù)信息。
字節(jié)碼校驗(yàn)
第三階段是整個(gè)驗(yàn)證過(guò)程最復(fù)雜的階段,主要目的是通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗(yàn)后,這個(gè)階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事情,例如:
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,例如不會(huì)出現(xiàn)類似這種情況:在操作棧放置了一個(gè)int類型的數(shù)據(jù),使用時(shí)卻按long類型來(lái)加載入本地變量表中。
- 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上。
- 保證方法體中的類型轉(zhuǎn)換是有效的,例如可以把一個(gè)子類數(shù)據(jù)類型,甚至把對(duì)象復(fù)制給它毫無(wú)繼承關(guān)系、完全不相干的一個(gè)數(shù)據(jù)類型,則是危險(xiǎn)和不合法的。
- ......
如果一個(gè)類方法體的字節(jié)碼沒(méi)又通過(guò)字節(jié)碼驗(yàn)證,那肯定是有問(wèn)題的:但如果一個(gè)方法體通過(guò)了字節(jié)碼驗(yàn)證,也不能說(shuō)明其一定就是安全的。即使字節(jié)碼驗(yàn)證之中進(jìn)行了大量的檢查,也不能保證這一點(diǎn)。這里涉及了離散數(shù)學(xué)中一個(gè)很著名的問(wèn)題“Halting Problem”:通俗一點(diǎn)講就是,通過(guò)程序去校驗(yàn)程序邏輯是無(wú)法做到絕對(duì)準(zhǔn)確的,不能通過(guò)程序準(zhǔn)確地檢查出程序是否能在有效的時(shí)間之內(nèi)結(jié)束運(yùn)行。
在JDK1.6的HotSpot虛擬機(jī)中提供了-XX:-UseSplitVerifier選項(xiàng)來(lái)關(guān)閉StackMapTable優(yōu)化,或者使用參數(shù)-XX:+FailOverToOldVerifier要求在類型校驗(yàn)失敗的時(shí)候退回到舊的類型推導(dǎo)方式進(jìn)行校驗(yàn)。而在JDK1.7之后,對(duì)于主版本號(hào)大于50的Class文件,使用類型檢查來(lái)完成數(shù)據(jù)流分析校驗(yàn)這是唯一的選擇,不允許再退回到類型推導(dǎo)的校驗(yàn)方式。
符號(hào)引用驗(yàn)證
最后一個(gè)階段的校驗(yàn)發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段——解析階段中發(fā)生。符號(hào)引用驗(yàn)證可以看做是對(duì)類自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn),通常需要校驗(yàn)下列內(nèi)容:
- 符號(hào)引用中通過(guò)字符串描述的全限定名是否能找到對(duì)應(yīng)的類
- 在指定類中是否存在符合方法的字段描述以及簡(jiǎn)單名稱所描述的方法和字段
- 符號(hào)引用中的類、字段、方法的訪問(wèn)性(private、protected、public、default)是否可以被當(dāng)前類訪問(wèn)
符號(hào)引用驗(yàn)證的目的是確保解析動(dòng)作能正常執(zhí)行,如果無(wú)法通過(guò)符號(hào)引用驗(yàn)證,那么將拋出java.lang.IncompatibleClassChangeError異常的子類,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
對(duì)于虛擬機(jī)的類加載機(jī)制來(lái)說(shuō),驗(yàn)證階段是一個(gè)非常重要的、但不是一定必要(因?yàn)閷?duì)程序運(yùn)行期沒(méi)有影響)的階段。如果所運(yùn)行的全部代碼都已經(jīng)被反復(fù)使用和驗(yàn)證過(guò),那么在實(shí)施階段就可以考慮使用-Xverify:none參數(shù)來(lái)關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)加載的時(shí)間。
準(zhǔn)備
準(zhǔn)備階段是正式的類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這個(gè)階段中兩個(gè)容易產(chǎn)生混淆的概念需要強(qiáng)調(diào)一下:
- 首先,這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中
- 其次,這里所說(shuō)的初始值“通常情況”下是數(shù)據(jù)類型的零值,假設(shè)一個(gè)類變量的定義為:
public static int value = 123;
那變量value在準(zhǔn)備階段過(guò)后的初始值為0而不是123,因?yàn)檫@時(shí)候尚未開始執(zhí)行任何Java方法,而把value賦值為123的pubstatic指令是程序被編譯之后,存放于類構(gòu)造器<clinit>()方法之中,所以把value賦值為123的動(dòng)作將在初始化階段才會(huì)執(zhí)行。下表列出了Java中所有基本數(shù)據(jù)類型的零值。
數(shù)據(jù)類型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | '\u0000' |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
上面提到,在通常情況下初始值是零值,那相對(duì)的會(huì)有一些特殊情況:如果類字段的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量value就會(huì)被初始化為ConstantValue屬性所指定的值,假設(shè)上面類變量value的定義變?yōu)椋?/p>
public static final int value = 123;
編譯時(shí)Javac將會(huì)為value生產(chǎn)ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue的設(shè)置將value賦值為123。
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程,在解析階段中所說(shuō)的直接引用和符號(hào)引用有什么關(guān)聯(lián)呢?
- 符號(hào)引用(Symbolic References):符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可。符號(hào)引用于虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無(wú)關(guān),引用的目標(biāo)不一定已經(jīng)加載到內(nèi)存中。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各不相同,但是它們能接受的符號(hào)引用必須都是一致的,因?yàn)榉?hào)引用的字面量形式明確定義在Java虛擬機(jī)規(guī)范的Class文件格式中。
- 直接引用(Direct References):直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號(hào)引用能在不同虛擬機(jī)實(shí)例上翻譯出來(lái)的直接引用一般不會(huì)相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
虛擬機(jī)規(guī)范之中并未規(guī)定解析階段發(fā)生的具體時(shí)間,只要求了在執(zhí)行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、Idc、Idc_w、multianewarry、new、putfield和putstatic這16個(gè)用于操作符號(hào)引用的字節(jié)碼指令之前,先對(duì)它們所使用的符號(hào)引用進(jìn)行解析。所以虛擬機(jī)可以根據(jù)需要來(lái)判斷到底是在類加載器加載時(shí)就對(duì)常量池中的符號(hào)引用進(jìn)行解析,還是等到一個(gè)符號(hào)引用將要被使用的前再去解析它。
對(duì)同一個(gè)符號(hào)引用進(jìn)行多次解析請(qǐng)求是很常見的事情,除了invokedynamic指令之外,虛擬機(jī)實(shí)現(xiàn)可以對(duì)第一次解析的結(jié)果進(jìn)行緩存(在運(yùn)行時(shí)常量池中記錄直接引用,并把常量標(biāo)識(shí)為已解析的狀態(tài))從而避免解析動(dòng)作重復(fù)進(jìn)行。無(wú)論是否真正執(zhí)行了多次解析動(dòng)作,虛擬機(jī)需要保證的是在同一實(shí)體中,如果一個(gè)符號(hào)引用之前已經(jīng)被成功解析過(guò),那么后續(xù)引用解析請(qǐng)求就應(yīng)當(dāng)一致成功;同樣的,如果第一次解析失敗了,那么其他指令對(duì)符號(hào)的解析請(qǐng)求也應(yīng)該收到相同的異常。
對(duì)于invokedynamic指令,項(xiàng)目規(guī)則則不成立。當(dāng)碰到某個(gè)前面已經(jīng)由invokedynamic指令觸發(fā)過(guò)解析的符號(hào)引用時(shí),并不意味著這個(gè)解析結(jié)果對(duì)于其他invokedynamicz指令也同樣生效。因?yàn)閕nvokedynamic指令的目的本來(lái)就是用于動(dòng)態(tài)語(yǔ)言支持(目前僅使用Java語(yǔ)言不會(huì)生成這條字節(jié)碼指令),它所對(duì)應(yīng)的引用稱為“動(dòng)態(tài)調(diào)用點(diǎn)限定符”(Dynamic Call Site Specifier),這里“動(dòng)態(tài)”的含義就是必須得到程序?qū)嶋H運(yùn)行到這條指令的時(shí)候,解析動(dòng)作才能進(jìn)行。相對(duì)的,其余可觸發(fā)解析的指令都是“靜態(tài)”的,可以在剛剛完成加載階段,還沒(méi)有開始執(zhí)行代碼時(shí)就進(jìn)行解析。
及誒西動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行,分別對(duì)應(yīng)于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info這7種常量類型。下面是4種引用的解析過(guò)程。
類和接口的解析
假設(shè)當(dāng)前代碼所處的類為D,如果要把一個(gè)從未解析過(guò)的符號(hào)引用N解析為一個(gè)類或接口C的直接引用,那虛擬機(jī)愛(ài)完成整改解析的過(guò)程需要以下3步:
- 如果C不是一個(gè)數(shù)組類型,那虛擬機(jī)將會(huì)把代表N的全限定名傳遞給D的類加載器去加載這個(gè)類C。在加載過(guò)程中,由于元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證的需要,又有可能觸發(fā)其他相關(guān)類的加載動(dòng)作,例如加載這個(gè)類的父類或?qū)崿F(xiàn)的接口。一旦這個(gè)加載過(guò)程出現(xiàn)了任何異常,解析過(guò)程就宣告失敗。
- 如果C是一個(gè)數(shù)組類型,并且數(shù)組的元素類型為對(duì)象,也就是N的描述符合會(huì)是類似“[Ljava/lang/Integer”的形式,那將會(huì)按照第1點(diǎn)的規(guī)則加載數(shù)組元素類型。如果N的描述符如前面所假設(shè)的形式,需要加載的元素類型就是“java.lang.Integer”,接著由虛擬機(jī)生成一個(gè)代表此數(shù)組維度和元素的數(shù)組對(duì)象。
- 如果上面的步驟沒(méi)有出現(xiàn)任何異常,那么C在虛擬機(jī)愛(ài)中實(shí)際上已經(jīng)成為一個(gè)有效的類或接口了,但在解析完成之七點(diǎn)還要進(jìn)行符號(hào)引用驗(yàn)證,確認(rèn)D是否具備對(duì)C的訪問(wèn)全下你。如果發(fā)現(xiàn)不具備訪問(wèn)權(quán)限,將拋出java.lang.IllegalAccessError異常。
字段解析
要解析一個(gè)未被解析過(guò)的字段符號(hào)引用,首先將會(huì)對(duì)字段表內(nèi)class_index項(xiàng)中索引的CONSTANT_Class_info符號(hào)引用進(jìn)行解析,也就是字段所屬的類或接口的符號(hào)引用。如果在解析這個(gè)類或接口符號(hào)引用的過(guò)程中出來(lái)了任何異常,都會(huì)導(dǎo)致字段符號(hào)引用解析的失敗。如果解析完成,那將這個(gè)字段所屬的類或接口用C表示,虛擬機(jī)規(guī)范要求按照如下步驟對(duì)C進(jìn)行后續(xù)字段的搜索:
- 如果C本身就包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這兩個(gè)字段的直接引用,查找結(jié)束。
- 否則,如果在C中實(shí)現(xiàn)了接口,將會(huì)你按照繼承關(guān)系從下往上遞歸搜索各個(gè)接口和它的父接口,如果接口中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用,查找結(jié)束。
- 否則,如果C不是java.lang.Object的話,將會(huì)按照繼承關(guān)系從下往上遞歸搜索其父類,如果在父類中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用,查找結(jié)束。
- 否則,查找失敗,拋出java.lang.NoSuchFiledError異常。
如果查找過(guò)程成功返回了引用,將會(huì)對(duì)這個(gè)字段進(jìn)行權(quán)限驗(yàn)證,如果發(fā)現(xiàn)不具備對(duì)字段的訪問(wèn)權(quán)限,將會(huì)拋出java.lang.IllegalAccessError異常。
在實(shí)際的應(yīng)用中,虛擬機(jī)的編譯器實(shí)現(xiàn)可能會(huì)比上述的要求更加嚴(yán)格,如果有一個(gè)同名字段同時(shí)出現(xiàn)在C的接口和父類中,或者同時(shí)在自己或父類的多個(gè)接口中出現(xiàn),那編譯器將可能拒絕編譯。如下的代碼將會(huì)被編譯器拒絕編譯這段代碼。
public class HH {
public static void main(String[] args) {
System.out.println(Sub.A);
}
interface Interface0 {
int A = 0;
}
interface Interface1 extends Interface0 {
int A = 1;
}
interface Interface2 {
int A = 2;
}
static class Parent implements Interface1 {
public static int A = 3;
}
static class Sub extends Parent implements Interface2 {}
}
類方法解析
類方法解析的第一個(gè)步驟與字段解析一樣,也需要先解析出類方法表的class_index項(xiàng)目索引的方法所屬的類或接口的符號(hào)引用,如果解析成功,我們依然用C表示這個(gè)類,接下來(lái)虛擬機(jī)將會(huì)按照如下步驟進(jìn)行后續(xù)的類方法搜索。
- 類方法和接口方法符號(hào)引用的常量類型定義是分開的,如果在類方法表中發(fā)現(xiàn)class_index中索引的C是個(gè)接口,那就直接跑出java.lang.IncompatibleClassChangeError異常。
- 如果通過(guò)了第一步,在類C中查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)匹配的方法,如果有則返回這個(gè)方法的直接引用,查找結(jié)束。
- 否則,在類C的父類中遞歸查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用,查找結(jié)束。
- 否則,在類C實(shí)現(xiàn)的接口列表及它們的父接口之中遞歸查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果存在匹配的方法,說(shuō)明類C是一個(gè)抽象類,這時(shí)查找結(jié)束,拋出java.lang.AbstractMethodError異常。
- 否則,宣告方法查找失敗,拋出java.lang.IllegalAccessError異常。
接口方法解析
接口方法也需要先解析出接口方法表的class_index項(xiàng)中索引的方法所屬的類或接口的符號(hào)引用,如果解析成功,依然用C表示這個(gè)接口,接下來(lái)虛擬機(jī)將會(huì)按照如下步驟進(jìn)行后續(xù)的接口方法搜索。
- 與類方法解析不同,如果在接口方法表中發(fā)現(xiàn)class_index中的索引C是個(gè)類而不是接口,那就直接拋出java.lang.IncompatibleClassChangeError異常。
- 否則,在接口C中查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用,查找結(jié)束。
- 否則,在接口C的父接口中遞歸查找,直到j(luò)ava.lang.Object類(查找范圍會(huì)包括Object類)位置,看是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用,查找結(jié)束。
- 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。
由于接口中的所有方法默認(rèn)都是public的,所以不存在訪問(wèn)權(quán)限的問(wèn)題,因此接口方法的符號(hào)解析應(yīng)當(dāng)不會(huì)拋出java.lang.IllegalAccessError異常。
初始化
類初始化階段類加載過(guò)程的最后一步,前面的類加載過(guò)程中,除了在加載階段用戶應(yīng)用程序可以通過(guò)自定義類加載器參與之外,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制。到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼或者說(shuō)是字節(jié)碼。
在準(zhǔn)備階段,變量已經(jīng)賦過(guò)一次系統(tǒng)要求的初始值,而在初始化階段,則根據(jù)程序員通過(guò)程序制定的主觀計(jì)劃去初始化類變量和其他資源,或者可以從另外一個(gè)角度來(lái)表達(dá):初始化階段是執(zhí)行類構(gòu)造器clinit()方法的過(guò)程。我們?cè)谙挛闹v解clinit()方法是怎么生成的,在這里,我們先看一下clinit()方法執(zhí)行過(guò)程中一些可能會(huì)影響程序運(yùn)行行為的特點(diǎn)和細(xì)節(jié),這部分相對(duì)更貼近于我們普通開發(fā)人員。
- clinit()方法是由編譯器你自動(dòng)收集類中的所有類變量的賦值操作和靜態(tài)語(yǔ)句塊(static{})中語(yǔ)句合并產(chǎn)生的,編譯器收集的順序是由語(yǔ)句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語(yǔ)句塊中只能訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語(yǔ)句塊可以復(fù)制,但是不能訪問(wèn),例如:
public class Test {
static {
i = 0;//給變量賦值可以正常編譯通過(guò)
System.out.print(i);//這句編譯器會(huì)提示“非法向前引用”
}
static int i = 0;
}
- clinit()方法與類的構(gòu)造函數(shù)(或者說(shuō)實(shí)例構(gòu)造器init()方法)不同,它不需要顯式地調(diào)用父類構(gòu)造器,虛擬機(jī)會(huì)保證在子類的clinit()方法執(zhí)行之前,父類的clinit()方法已經(jīng)執(zhí)行完畢。因此在虛擬機(jī)中第一個(gè)被執(zhí)行的clinit()方法的類肯定是java.lang.Object。
- 由于父類的clinit()方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語(yǔ)句塊要優(yōu)于子類的變量賦值操作,如下代碼中所示:
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
- clinit()方法對(duì)于類或接口來(lái)說(shuō)并不是必須的,如果一個(gè)類中沒(méi)有靜態(tài)語(yǔ)句塊,也沒(méi)有對(duì)變量的賦值操作,那么編譯期可以不為這個(gè)類生成clinit()方法。
- 接口不能使用靜態(tài)語(yǔ)句塊,但仍然有變量初始化的操作,因此接口與類一樣都會(huì)生成clinit()方法。但接口與類不同的是,執(zhí)行接口的clinit()方法不需要先執(zhí)行父接口clinit()方法。只有當(dāng)父接口中定義的變量使用時(shí),父接口才會(huì)初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也是一樣不會(huì)執(zhí)行接口的clinit()方法。
- 虛擬機(jī)會(huì)保證一個(gè)類clinit()方法在多線程環(huán)境中被正確地加鎖、同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的clinit()方法,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行clinit()方法完畢。如果在一個(gè)類的clinit()方法中有耗時(shí)很長(zhǎng)的操作,就可能造成多個(gè)進(jìn)程阻塞,在實(shí)際應(yīng)用中這種阻塞往往是很隱蔽的。代碼如下:
public class Main {
public static void main(String[] args) {
Runnable script = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + " start");
DeadLoopClass main = new DeadLoopClass();
System.out.println(Thread.currentThread() + " run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
static class DeadLoopClass {
static {
//如果不加上if語(yǔ)句,編譯期將提示“Initializer does not complete normally”并拒絕編譯
if (true) {
System.out.println(Thread.currentThread() + "init DeadLoopClass");
while (true) {
}
}
}
}
}
運(yùn)行結(jié)果如下,即一條線程在死循環(huán)以長(zhǎng)時(shí)間操作,另外一條線程在阻塞等待。
類加載器
虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把類加載階段中的“通過(guò)一個(gè)類的全限定名在獲取描述此類的二進(jìn)制字節(jié)流”這個(gè)動(dòng)作放在JVM外部去實(shí)現(xiàn),以便讓應(yīng)用應(yīng)用程序自己決定如何去獲取所需要的類,實(shí)現(xiàn)這個(gè)動(dòng)作的代碼模塊稱為“類加載器”。
類加載器是Java語(yǔ)言的一項(xiàng)創(chuàng)新,也是Java語(yǔ)言流行的重要原因之一,它最初是為了滿足Java Applet的需求而開發(fā)出來(lái)的。雖然目前Java Applet技術(shù)基本上應(yīng)死掉了,但是類加載器卻在類層次劃分,OSGi、熱部署、代碼加密等領(lǐng)域大放異彩,成為了Java技術(shù)體系中一塊重要的基石,可謂失之桑榆,收之東隅。
類與類加載器
類加載器雖然只用于實(shí)現(xiàn)類的加載動(dòng)作,但它在Java程序中起到的作用卻遠(yuǎn)遠(yuǎn)不限于類加載階段,對(duì)于任意一個(gè)類,都需要又加載它的類加載器和這個(gè)類本身一同確立其在JVM中的唯一性,每一個(gè)加載器,都擁有一個(gè)獨(dú)立的類命名空間。這句話可以表達(dá)的更通俗一些:比較兩個(gè)類是否“相等”,只有在這兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義,否則,即時(shí)這兩個(gè)類來(lái)源于同一個(gè)Class文件,被同一個(gè)虛擬機(jī)加載,只要加載它們的類加載器不同,那這兩個(gè)類就必定不相等。
這里指的“相等”包括代表類的Class對(duì)象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結(jié)果,也包括使用instanceof關(guān)鍵字做對(duì)象所屬關(guān)系判定等情況。如果沒(méi)有注意到類加載器的影響,在某些情況下可能會(huì)產(chǎn)生具有迷惑性的結(jié)果。
public class Main {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
System.out.println(fileName);
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("Main").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof Main);
}
}
運(yùn)行結(jié)果:
class Main
false
上面構(gòu)造了一個(gè)簡(jiǎn)單的類加載器,盡管很簡(jiǎn)單,但是對(duì)于這個(gè)演示來(lái)說(shuō)還是夠用的。它可以加載與自己在同一路徑下的Class文件。我們使用這個(gè)類加載器去加載了一個(gè)名為“Main”的類,并實(shí)例化了這個(gè)類的對(duì)象。兩行輸出結(jié)果中,從第一句可以看出,這個(gè)對(duì)象確實(shí)是類Main實(shí)例化出來(lái)的對(duì)象,但是從第二句就可以發(fā)現(xiàn),這個(gè)對(duì)象與類Main所屬類型檢查的時(shí)候卻返回了false,這是因?yàn)樘摂M機(jī)中存在兩個(gè)Main類,一個(gè)是由新途觀應(yīng)用程序加載器加載的,另外一個(gè)是由我們自定義的類加載器加載的,雖然兩個(gè)來(lái)自同一個(gè)Class文件,但依然是兩個(gè)獨(dú)立的類,做對(duì)象所屬類型檢查時(shí)結(jié)果自然是false。
雙親委派模型
從JVM的角度來(lái)講,只存在兩種不同的類加載器:一種是啟動(dòng)類加載器(Bootstrap ClassLoader),這個(gè)加載器使用C++語(yǔ)言實(shí)現(xiàn),是虛擬機(jī)自身的一部分;另一種就是所有其他的類加載器,這些類加載器是Java語(yǔ)言實(shí)現(xiàn)的,并且全都繼承自抽象類java.lang.ClassLoader。
從Java開發(fā)人員的角度來(lái)看,類加載器還可以被劃分的更細(xì)致,絕大部分Java程序都會(huì)使用到以下3種由系統(tǒng)提供的類加載器。
- 啟動(dòng)類加載器(Bootstrap ClassLoader):前面已經(jīng)介紹過(guò),這個(gè)類加載器負(fù)責(zé)將存放在JAVA_HOME\lib目錄總的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)識(shí)別的(僅按照文件名識(shí)別,入rt.jar,名字不符合的類庫(kù)即使放在lib目錄也不會(huì)被加載)類庫(kù)加載到虛擬機(jī)內(nèi)存中。啟動(dòng)類加載類無(wú)法被Java程序直接引用,用戶在編寫自定義類加載器時(shí),如果需要把加載請(qǐng)求為派給引導(dǎo)類加載器,那直接使用null代替即可,如下所示:
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}
- 擴(kuò)展類加載器(Extension ClassLoader):這個(gè)加載器由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),它負(fù)責(zé)加載JAVA_HOME\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫(kù),開發(fā)者可以直接使用擴(kuò)展類加載器。
- 應(yīng)用程序類加載器(Application ClassLoader):這個(gè)類加載器由sun.misc.Launcher$AppClassLoader實(shí)現(xiàn)。由于這個(gè)類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統(tǒng)類加載器。它負(fù)責(zé)加載用戶路徑(ClassPath)上所指定的類庫(kù),開發(fā)者可以直接使用這個(gè)類加載器,如果應(yīng)用程序中沒(méi)有自定義過(guò)自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。
我們的應(yīng)用程序都是由這三種類加載器相互配合進(jìn)行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器之間的關(guān)系一般如圖所示:
圖中展示的類加載器之間的這種層次關(guān)系,稱為類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。這里類加載器的父子關(guān)系一般不會(huì)以繼承的關(guān)系來(lái)實(shí)現(xiàn),而是都使用組合(Composition)關(guān)系來(lái)復(fù)用父加載器的代碼。
雙親委派模型的工作過(guò)程是:如果一個(gè)類加載器收到了類加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的類加載請(qǐng)求最終都應(yīng)該傳到頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒(méi)有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載。
使用雙親委派模型來(lái)組織類加載器之間的關(guān)系,有一個(gè)顯而易見的好處就是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系。例如類java.lang.Object,它存放在rt.jar之中,無(wú)論哪一個(gè)類加載器要加載這個(gè)類,最終都會(huì)委派給處于模型最頂端的啟動(dòng)類加載器進(jìn)行加載,僅此Object類在程序的各種類加載環(huán)境中都是一個(gè)類。相反的,如果沒(méi)有使用雙親委派模型,由各個(gè)類加載器自行去加載的話,如果用戶自己編寫了一個(gè)稱為java.lang.Object的類,并放在程序的ClassPath中,那系統(tǒng)中將會(huì)出現(xiàn)多個(gè)不同的Object類,Java類型體系中最基礎(chǔ)的類也就無(wú)法保證,應(yīng)用程序也將會(huì)變的一片混亂。
雙親委派模型對(duì)于保證Java程序的穩(wěn)定運(yùn)行很重要,但它的實(shí)現(xiàn)卻非常簡(jiǎn)單,實(shí)現(xiàn)雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法之中,代碼如下,邏輯很簡(jiǎn)單:先檢查是否已經(jīng)被加載過(guò),弱沒(méi)有則調(diào)用父加載器的loadClass()方法,弱父加載器為空則默認(rèn)使用啟動(dòng)類加載器作為父加載器。如果父類加載失敗,拋出ClassNotFoundException異常后,再調(diào)用自己的findClass()方法進(jìn)行加載。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}