代碼編譯的結(jié)果從本地機器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲格式發(fā)展的一小步,卻是編程語言發(fā)展的一大步。
前面我們學(xué)習(xí)了Class文件存儲格式的具體細節(jié)咒精,在Class文件中描述的各類信息,最終都需要加載到虛擬機中之后才能被運行和使用旷档。而虛擬機如何加載這些Class文件,Class文件中的信息進入到虛擬機后會發(fā)生什么變化歇拆,這些都是將要講解的內(nèi)容鞋屈。
Java虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗故觅、轉(zhuǎn)換解析和初始化厂庇,最終形成可以被虛擬機直接使用的Java類型,這個過程被稱作虛擬機的類加載機制输吏。
與那些在編譯時需要進行連接的語言不同权旷,在Java語言里面,類型的加載贯溅、連接和初始化過程都是在程序運行期間完成的拄氯,這種策略讓Java語言進行提前編譯會面臨額外的困難,也會讓類加載時稍微增加一些性能開銷它浅,但是卻為Java應(yīng)用提供了極高的擴展性和靈活性译柏,Java天生可以動態(tài)擴展的語言特性就是依賴運行期動態(tài)加載和動態(tài)連接這個特點實現(xiàn)的。姐霍。例如鄙麦,編寫一個面向接口的應(yīng)用程序,可以等到運行時再指定其實際的實現(xiàn)類镊折,用戶可以通過Java預(yù)置的或自定義類加載器胯府,讓某個本地的應(yīng)用程序在運行時從網(wǎng)絡(luò)或其他地方上加載一個二進制流作為其程序代碼的一部分。這種動態(tài)組裝應(yīng)用的方式目前已廣泛應(yīng)用于Java程序之中恨胚,從最基礎(chǔ)的Applet骂因、JSP到相對復(fù)雜的OSGi技術(shù),都依賴著Java語言運行期類加載才得以誕生与纽。
為了避免語言表達中可能產(chǎn)生的偏差侣签,下面我們先設(shè)立兩個語言上的約定:
- 第一,在實際情況中急迂,每個Class文件都有代表著Java語言中的一個類或接口的可能影所,后文中直接對“類型”的描述都同時蘊含著類和接口的可能性,而需要對類和接口分開描述的場景僚碎,會特別指明猴娩;
- 第二,與前面介紹Class文件格式時的約定一致,本章所提到的“Class文件”也并非特指某個存在于具體磁盤中的文件卷中,而應(yīng)當是一串二進制字節(jié)流矛双,無論其以何種形式存在,包括但不限于磁盤文件蟆豫、網(wǎng)絡(luò)议忽、數(shù)據(jù)庫、內(nèi)存或者動態(tài)產(chǎn)生等十减。
類加載的時機
一個類型從被加載到虛擬機內(nèi)存中開始栈幸,到卸載出內(nèi)存為止,它的整個生命周期將會經(jīng)歷加載(Loading)帮辟、驗證(Verification)速址、準備(Preparation)、解析(Resolution)由驹、初始化(Initialization)芍锚、使用(Using)和卸載(Unloading)七個階段,其中驗證蔓榄、準備并炮、解析三個部分統(tǒng)稱為連接(Linking)。這七個階段的發(fā)生順序如圖7-1所示润樱。
圖7-1中渣触,加載、驗證壹若、準備嗅钻、初始化和卸載這五個階段的順序是確定的,類型的加載過程必須按照這種順序按部就班地開始店展,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始养篓,這是為了支持Java語言的運行時綁定特性(也稱為動態(tài)綁定或晚期綁定)。
關(guān)于在什么情況下需要開始類加載過程的第一個階段“加載”赂蕴,《Java虛擬機規(guī)范》中并沒有進行強制約束柳弄,這點可以交給虛擬機的具體實現(xiàn)來自由把握。但是對于初始化階段概说,《Java虛擬機規(guī)范》則是嚴格規(guī)定了有且只有六種情況必須立即對類進行“初始化”(而加載碧注、驗證、準備自然需要在此之前開始):
1)遇到new糖赔、getstatic萍丐、putstatic或invokestatic這四條字節(jié)碼指令時,如果類型沒有進行過初始化放典,則需要先觸發(fā)其初始化階段逝变。能夠生成這四條指令的典型Java代碼場景有:
- 使用new關(guān)鍵字實例化對象的時候基茵。
- 讀取或設(shè)置一個類型的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候壳影。
- 調(diào)用一個類型的靜態(tài)方法的時候拱层。
2)使用java.lang.reflect包的方法對類型進行反射調(diào)用的時候,如果類型沒有進行過初始化宴咧,則需要先觸發(fā)其初始化根灯。
3)當初始化類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化掺栅,則需要先觸發(fā)其父類的初始化箱吕。
4)當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類)柿冲,虛擬機會先初始化這個主類。
5)當使用JDK 7新加入的動態(tài)語言支持時兆旬,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果為REF_getStatic假抄、REF_putStatic、REF_invokeStatic丽猬、REF_newInvokeSpecial四種類型的方法句
柄宿饱,并且這個方法句柄對應(yīng)的類沒有進行過初始化,則需要先觸發(fā)其初始化脚祟。
6)當一個接口中定義了JDK 8新加入的默認方法(被default關(guān)鍵字修飾的接口方法)時谬以,如果有這個接口的實現(xiàn)類發(fā)生了初始化,那該接口要在其之前被初始化由桌。
對于這六種會觸發(fā)類型進行初始化的場景为黎,《Java虛擬機規(guī)范》中使用了一個非常強烈的限定語——“有且只有”,這六種場景中的行為稱為對一個類型進行主動引用行您。除此之外铭乾,所有引用類型的方式都不會觸發(fā)初始化,稱為被動引用娃循。
代碼清單7-1 被動引用的例子之一
package org.fenixsoft.classloading;
/**
* 被動使用類字段演示一:
* 通過子類引用父類的靜態(tài)字段炕檩,不會導(dǎo)致子類初始化
**/
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 class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
上述代碼運行之后,只會輸出“SuperClass init捌斧!”笛质,而不會輸出“SubClass init!”捞蚂。對于靜態(tài)字段妇押,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態(tài)字段洞难,只會觸發(fā)父類的初始化而不會觸發(fā)子類的初始化.
代碼清單7-2 被動引用的例子之二
package org.fenixsoft.classloading;
/**
* 被動使用類字段演示二:
* 通過數(shù)組定義來引用類舆吮,不會觸發(fā)此類的初始化
**/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
為了節(jié)省版面揭朝,這段代碼復(fù)用了代碼清單7-1中的SuperClass,運行之后發(fā)現(xiàn)沒有輸出“SuperClass init色冀!”潭袱,說明并沒有觸發(fā)類org.fenixsoft.classloading.SuperClass的初始化階段。但是這段代碼里面觸發(fā)了
另一個名為“[Lorg.fenixsoft.classloading.SuperClass”的類的初始化階段锋恬,對于用戶代碼來說屯换,這并不是一個合法的類型名稱,它是一個由虛擬機自動生成的与学、直接繼承于java.lang.Object的子類彤悔,創(chuàng)建動作由字節(jié)碼指令newarray觸發(fā)。
這個類代表了一個元素類型為org.fenixsoft.classloading.SuperClass的一維數(shù)組索守,數(shù)組中應(yīng)有的屬性和方法(用戶可直接使用的只有被修飾為public的length屬性和clone()方法)都實現(xiàn)在這個類里晕窑。Java語言中對數(shù)組的訪問要比C/C++相對安全,很大程度上就是因為這個類包裝了數(shù)組元素的訪問卵佛,而C/C++中則是直接翻譯為對數(shù)組指針的移動杨赤。在Java語言里,當檢查到發(fā)生數(shù)組越界時會拋出
java.lang.ArrayIndexOutOfBoundsException異常截汪,避免了直接造成非法內(nèi)存訪問疾牲。
代碼清單7-3 被動引用的例子之三
package org.fenixsoft.classloading;
/**
* 被動使用類字段演示三:
* 常量在編譯階段會存入調(diào)用類的常量池中,本質(zhì)上沒有直接引用到定義常量的類衙解,因此不會觸發(fā)定義常量的
類的初始化
**/
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
/**
* 非主動使用類字段演示
**/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
上述代碼運行之后阳柔,也沒有輸出“ConstClass init!”蚓峦,這是因為雖然在Java源碼中確實引用了ConstClass類的常量HELLOWORLD舌剂,但其實在編譯階段通過常量傳播優(yōu)化,已經(jīng)將此常量的值“hello world”直接存儲在NotInitialization類的常量池中暑椰,以后NotInitialization對常量ConstClass.HELLOWORLD的引用架诞,實際都被轉(zhuǎn)化為NotInitialization類對自身常量池的引用了。也就是說干茉,實際上NotInitialization的Class文件之中并沒有ConstClass類的符號引用入口谴忧,這兩個類在編譯成Class文件后就已不存在任何聯(lián)系了。
接口的加載過程與類加載過程稍有不同角虫,針對接口需要做一些特殊說明:接口也有初始化過程沾谓,這點與類是一致的,上面的代碼都是用靜態(tài)語句塊“static{}”來輸出初始化信息的戳鹅,而接口中不能使用“static{}”語句塊均驶,但編譯器仍然會為接口生成“<clinit>()”類構(gòu)造器,用于初始化接口中所定義的成員變量枫虏。接口與類真正有所區(qū)別的是前面講述的六種“有且僅有”需要觸發(fā)初始化場景中的第三種:當一個類在初始化時妇穴,要求其父類全部都已經(jīng)初始化過了爬虱,但是一個接口在初始化時,并不要求其父接口全部都完成了初始化腾它,只有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化跑筝。
類加載的過程
“加載”(Loading)階段是整個“類加載”(Class Loading)過程中的一個階段,在加載階段,Java虛擬機需要完成以下三件事情:
1)通過一個類的全限定名來獲取定義此類的二進制字節(jié)流瞒滴。
2)將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)曲梗。
3)在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口妓忍。
《Java虛擬機規(guī)范》對這三點要求其實并不是特別具體虏两,留給虛擬機實現(xiàn)與Java應(yīng)用的靈活度都是相當大的。例如“通過一個類的全限定名來獲取定義此類的二進制字節(jié)流”這條規(guī)則世剖,它并沒有指明二進制字節(jié)流必須得從某個Class文件中獲取定罢,確切地說是根本沒有指明要從哪里獲取、如何獲取旁瘫。僅僅這一點空隙引颈,Java虛擬機的使用者們就可以在加載階段搭構(gòu)建出一個相當開放廣闊的舞臺,Java發(fā)展歷程中境蜕,充滿創(chuàng)造力的開發(fā)人員則在這個舞臺上玩出了各種花樣,許多舉足輕重的Java技術(shù)都建立在這一基礎(chǔ)之上凌停,例如:
- 從ZIP壓縮包中讀取粱年,這很常見,最終成為日后JAR罚拟、EAR台诗、WAR格式的基礎(chǔ)。
- 從網(wǎng)絡(luò)中獲取赐俗,這種場景最典型的應(yīng)用就是Web Applet拉队。
- 運行時計算生成,這種場景使用得最多的就是動態(tài)代理技術(shù)阻逮,在java.lang.reflect.Proxy中粱快,就是用了ProxyGenerator.generateProxyClass()來為特定接口生成形式為“*$Proxy”的代理類的二進制字節(jié)流。
....
相對于類加載過程的其他階段叔扼,非數(shù)組類型的加載階段(準確地說事哭,是加載階段中獲取類的二進制字節(jié)流的動作)是開發(fā)人員可控性最強的階段。加載階段既可以使用Java虛擬機里內(nèi)置的引導(dǎo)類加載器來完成瓜富,也可以由用戶自定義的類加載器去完成鳍咱,開發(fā)人員通過定義自己的類加載器去控制字節(jié)流的獲取方式(重寫一個類加載器的findClass()或loadClass()方法),實現(xiàn)根據(jù)自己的想法來賦予應(yīng)用
程序獲取運行代碼的動態(tài)性与柑。
對于數(shù)組類而言谤辜,情況就有所不同蓄坏,數(shù)組類本身不通過類加載器創(chuàng)建,它是由Java虛擬機直接在內(nèi)存中動態(tài)構(gòu)造出來的丑念。但數(shù)組類與類加載器仍然有很密切的關(guān)系涡戳,因為數(shù)組類的元素類型(ElementType,指的是數(shù)組去掉所有維度的類型)最終還是要靠類加載器來完成加載.
加載階段結(jié)束后渠欺,Java虛擬機外部的二進制字節(jié)流就按照虛擬機所設(shè)定的格式存儲在方法區(qū)之中了妹蔽,方法區(qū)中的數(shù)據(jù)存儲格式完全由虛擬機實現(xiàn)自行定義,《Java虛擬機規(guī)范》未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)挠将。類型數(shù)據(jù)妥善安置在方法區(qū)之后胳岂,會在Java堆內(nèi)存中實例化一個java.lang.Class類的對象,這個對象將作為程序訪問方法區(qū)中的類型數(shù)據(jù)的外部接口舔稀。
驗證
驗證是連接階段的第一步乳丰,這一階段的目的是確保Class文件的字節(jié)流中包含的信息符合《Java虛擬機規(guī)范》的全部約束要求,保證這些信息被當作代碼運行后不會危害虛擬機自身的安全内贮。
Java語言本身是相對安全的編程語言(起碼對于C/C++來說是相對安全的)产园,使用純粹的Java代碼無法做到諸如訪問數(shù)組邊界以外的數(shù)據(jù)、將一個對象轉(zhuǎn)型為它并未實現(xiàn)的類型夜郁、跳轉(zhuǎn)到不存在的代碼行之類的事情什燕,如果嘗試這樣去做了,編譯器會毫不留情地拋出異常竞端、拒絕編譯屎即。但前面也曾說過,Class文件并不一定只能由Java源碼編譯而來事富,它可以使用包括靠鍵盤0和1直接在二進制編輯器中敲出
Class文件在內(nèi)的任何途徑產(chǎn)生技俐。上述Java代碼無法做到的事情在字節(jié)碼層面上都是可以實現(xiàn)的,至少語義上是可以表達出來的统台。Java虛擬機如果不檢查輸入的字節(jié)流雕擂,對其完全信任的話,很可能會因為載入了有錯誤或有惡意企圖的字節(jié)碼流而導(dǎo)致整個系統(tǒng)受攻擊甚至崩潰贱勃,所以驗證字節(jié)碼是Java虛擬機保護自身的一項必要措施井赌。
驗證階段是非常重要的,這個階段是否嚴謹贵扰,直接決定了Java虛擬機是否能承受惡意代碼的攻擊族展,從代碼量和耗費的執(zhí)行性能的角度上講,驗證階段的工作量在虛擬機的類加載過程(注意區(qū)分和類的加載階段)中占了相當大的比重拔鹰。
1.文件格式驗證
第一階段要驗證字節(jié)流是否符合Class文件格式的規(guī)范仪缸,并且能被當前版本的虛擬機處理。這一階段可能包括下面這些驗證點:
·是否以魔數(shù)0xCAFEBABE開頭列肢。
·主恰画、次版本號是否在當前Java虛擬機接受范圍之內(nèi)宾茂。
·常量池的常量中是否有不被支持的常量類型(檢查常量tag標志)。
·指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量拴还。
·CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的數(shù)據(jù)跨晴。
·Class文件中各個部分及文件本身是否有被刪除的或附加的其他信息。
實際上第一階段的驗證點還遠不止這些片林,上面所列的只是從HotSpot虛擬機源碼中摘抄的一小部分內(nèi)容端盆,該驗證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲于方法區(qū)之內(nèi),格式上符合描述一個Java類型信息的要求费封。這階段的驗證是基于二進制字節(jié)流進行的焕妙,只有通過了這個階段的驗證之后,這段字節(jié)流才被允許進入Java虛擬機內(nèi)存的方法區(qū)中進行存儲弓摘,所以后面的三個驗證階段全部是基于方法區(qū)的存儲結(jié)構(gòu)上進行的焚鹊,不會再直接讀取、操作字節(jié)流了韧献。
2.元數(shù)據(jù)驗證
第二階段是對字節(jié)碼描述的信息進行語義分析末患,以保證其描述的信息符合《Java語言規(guī)范》的要求,這個階段可能包括的驗證點如下:
·這個類是否有父類(除了java.lang.Object之外锤窑,所有的類都應(yīng)當有父類)璧针。
·這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。
·如果這個類不是抽象類渊啰,是否實現(xiàn)了其父類或接口之中要求實現(xiàn)的所有方法探橱。
·類中的字段、方法是否與父類產(chǎn)生矛盾(例如覆蓋了父類的final字段虽抄,或者出現(xiàn)不符合規(guī)則的方
法重載,例如方法參數(shù)都一致独柑,但返回值類型卻不同等)迈窟。
3.字節(jié)碼驗證
第三階段是整個驗證過程中最復(fù)雜的一個階段,主要目的是通過數(shù)據(jù)流分析和控制流分析忌栅,確定程序語義是合法的车酣、符合邏輯的。在第二階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型校驗完畢以后索绪,這階段就要對類的方法體(Class文件中的Code屬性)進行校驗分析湖员,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為,例如:
- ·保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作瑞驱,例如不會出現(xiàn)類似于“在操作棧放置了一個int類型的數(shù)據(jù)娘摔,使用時卻按long類型來加載入本地變量表中”這樣的情況。
- 保證任何跳轉(zhuǎn)指令都不會跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上唤反。
- 保證方法體中的類型轉(zhuǎn)換總是有效的凳寺,例如可以把一個子類對象賦值給父類數(shù)據(jù)類型鸭津,這是安全的,但是把父類對象賦值給子類數(shù)據(jù)類型肠缨,甚至把對象賦值給與它毫無繼承關(guān)系逆趋、完全不相干的一個數(shù)據(jù)類型,則是危險和不合法的晒奕。
如果一個類型中有方法體的字節(jié)碼沒有通過字節(jié)碼驗證闻书,那它肯定是有問題的;但如果一個方法體通過了字節(jié)碼驗證脑慧,也仍然不能保證它一定就是安全的魄眉。即使字節(jié)碼驗證階段中進行了再大量、再嚴密的檢查漾橙,也依然不能保證這一點杆融。即不能通過程序準確地檢查出程序是否能在有限的時間之內(nèi)結(jié)束運行。在我們討論字節(jié)碼校驗的上下文語境里霜运,通俗一點的解釋是通過程序去校驗程序邏輯是無法做到絕對準確的脾歇,不可能用程序來準確判定一段程序是否存在Bug。
由于數(shù)據(jù)流分析和控制流分析的高度復(fù)雜性淘捡,Java虛擬機的設(shè)計團隊為了避免過多的執(zhí)行時間消耗在字節(jié)碼驗證階段中藕各,在JDK 6之后的Javac編譯器和Java虛擬機里進行了一項聯(lián)合優(yōu)化,把盡可能多的校驗輔助措施挪到Javac編譯器里進行焦除。具體做法是給方法體Code屬性的屬性表中新增加了一項名為“StackMapTable”的新屬性激况,這項屬性描述了方法體所有的基本塊(Basic Block,指按照控制流拆分的代碼塊)開始時本地變量表和操作棧應(yīng)有的狀態(tài)膘魄,在字節(jié)碼驗證期間乌逐,Java虛擬機就不需要根據(jù)程序推導(dǎo)這些狀態(tài)的合法性,只需要檢查StackMapTable屬性中的記錄是否合法即可创葡。這樣就將字節(jié)碼驗證的類型推導(dǎo)轉(zhuǎn)變?yōu)轭愋蜋z查浙踢,從而節(jié)省了大量校驗時間。理論上StackMapTable屬性也存在錯誤或被篡改的可能灿渴,所以是否有可能在惡意篡改了Code屬性的同時洛波,也生成相應(yīng)的StackMapTable屬性來騙過虛擬機的類型校驗,則是虛擬機設(shè)計者們需要仔細思考的問題骚露。
4.符號引用驗證
最后一個階段的校驗行為發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用[3]的時候蹬挤,這個轉(zhuǎn)化動作將在連接的第三階段——解析階段中發(fā)生。符號引用驗證可以看作是對類自身以外(常量池中的各種符號引用)的各類信息進行匹配性校驗棘幸,通俗來說就是焰扳,該類是否缺少或者被禁止訪問它依賴的某些外部類蹂随、方法滞诺、字段等資源肄程。本階段通常需要校驗下列內(nèi)容:
- 符號引用中通過字符串描述的全限定名是否能找到對應(yīng)的類贯钩。
- 在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。
- 符號引用中的類畜份、字段诞帐、方法的可訪問性(private、protected爆雹、public停蕉、<package>)是否可被當前類訪問。
·……
符號引用驗證的主要目的是確保解析行為能正常執(zhí)行钙态,如果無法通過符號引用驗證慧起,Java虛擬機將會拋出一個java.lang.IncompatibleClassChangeError的子類異常,典型的如:java.lang.IllegalAccessError册倒、java.lang.NoSuchFieldError蚓挤、java.lang.NoSuchMethodError等。
驗證階段對于虛擬機的類加載機制來說驻子,是一個非常重要的灿意、但卻不是必須要執(zhí)行的階段,因為驗證階段只有通過或者不通過的差別崇呵,只要通過了驗證缤剧,其后就對程序運行期沒有任何影響了。如果程序運行的全部代碼(包括自己編寫的域慷、第三方包中的荒辕、從外部加載的、動態(tài)生成的等所有代碼)都已經(jīng)被反復(fù)使用和驗證過犹褒,在生產(chǎn)環(huán)境的實施階段就可以考慮使用-Xverify:none參數(shù)來關(guān)閉大部分的
類驗證措施抵窒,以縮短虛擬機類加載的時間。
準備
準備階段是正式為類中定義的變量(即靜態(tài)變量叠骑,被static修飾的變量)分配內(nèi)存并設(shè)置類變量初始值的階段李皇,從概念上講,這些變量所使用的內(nèi)存都應(yīng)當在方法區(qū)中進行分配座云,但必須注意到方法區(qū)本身是一個邏輯上的區(qū)域疙赠,在JDK 7及之前付材,HotSpot使用永久代來實現(xiàn)方法區(qū)時朦拖,實現(xiàn)是完全符合這種邏輯概念的;而在JDK 8及之后厌衔,類變量則會隨著Class對象一起存放在Java堆中璧帝,這時候“類變量在方法區(qū)”就完全是一種對邏輯概念的表述了
關(guān)于準備階段,還有兩個容易產(chǎn)生混淆的概念筆者需要著重強調(diào)富寿,首先是這時候進行內(nèi)存分配的僅包括類變量睬隶,而不包括實例變量锣夹,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。其次是這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值苏潜,假設(shè)一個類變量的定義為:
public static int value = 123;
那變量value在準備階段過后的初始值為0而不是123银萍,因為這時尚未開始執(zhí)行任何Java方法,而把value賦值為123的putstatic指令是程序被編譯后恤左,存放于類構(gòu)造器<clinit>()方法之中贴唇,所以把value賦值為123的動作要到類的初始化階段才會被執(zhí)行。
準備階段還會構(gòu)造一些數(shù)據(jù)結(jié)構(gòu)飞袋,比如方法表戳气,一個包含方法指針的數(shù)組
jvm may also allocate memory for data structures that are intended to improve the performance of the running program. an example of such data structure is a method table which contains a pointer to the data for every method in a class, including those inherited from its super classes. which enables an inherited method to be invoked on an object without a search of superclasses at the point of invocation.
解析
解析階段是Java虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程,符號引用在前面講Class文件格式的時候已經(jīng)出現(xiàn)過多次巧鸭,在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)存當中的內(nèi)容锻拘。各種虛擬機實現(xiàn)的內(nèi)存布局可以各不相同油吭,但是它們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在《Java虛擬機規(guī)
范》的Class文件格式中署拟。
直接引用(Direct References):直接引用是可以直接指向目標的指針婉宰、相對偏移量或者是一個能間接定位到目標的句柄。直接引用是和虛擬機實現(xiàn)的內(nèi)存布局直接相關(guān)的推穷,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同心包。如果有了直接引用,那引用的目標必定已經(jīng)在虛擬機的內(nèi)存中存在馒铃。
《Java虛擬機規(guī)范》之中并未規(guī)定解析階段發(fā)生的具體時間蟹腾,只要求了在執(zhí)行ane-warray、checkcast区宇、getfield娃殖、getstatic、instanceof议谷、invokedynamic炉爆、invokeinterface、invoke-special、invokestatic芬首、invokevirtual赴捞、ldc、ldc_w郁稍、ldc2_w赦政、multianewarray、new耀怜、putfield和putstatic這17個用于操作符號引用的字節(jié)碼指令之前昼钻,先對它們所使用的符號引用進行解析。所以虛擬機實現(xiàn)可以根據(jù)需要來自行判斷封寞,到底是在類被加載器加載時就對常量池中的符號引用進行解析然评,還是等到一個符號引用將要被使用前才去解析它。
對同一個符號引用進行多次解析請求是很常見的事情狈究,除invokedynamic指令以外碗淌,虛擬機實現(xiàn)可以對第一次解析的結(jié)果進行緩存,譬如在運行時直接引用常量池中的記錄抖锥,并把常量標識為已解析狀態(tài)亿眠,從而避免解析動作重復(fù)進行。無論是否真正執(zhí)行了多次解析動作磅废,Java虛擬機都需要保證的是在同一個實體中纳像,如果一個符號引用之前已經(jīng)被成功解析過,那么后續(xù)的引用解析請求就應(yīng)當一直能夠
成功拯勉;同樣地竟趾,如果第一次解析失敗了,其他指令對這個符號的解析請求也應(yīng)該收到相同的異常宫峦,
不過對于invokedynamic指令岔帽,上面的規(guī)則就不成立了。當碰到某個前面已經(jīng)由invokedynamic指令觸發(fā)過解析的符號引用時导绷,并不意味著這個解析結(jié)果對于其他invokedynamic指令也同樣生效犀勒。因為invokedynamic指令的目的本來就是用于動態(tài)語言支持,它對應(yīng)的引用稱為“動態(tài)調(diào)用點限定符
(Dynamically-Computed Call Site Specifier)”妥曲,這里“動態(tài)”的含義是指必須等到程序?qū)嶋H運行到這條指令時贾费,解析動作才能進行。相對地檐盟,其余可觸發(fā)解析的指令都是“靜態(tài)”的褂萧,可以在剛剛完成加載階段,還沒有開始執(zhí)行代碼時就提前進行解析遵堵。
解析動作主要針對類或接口箱玷、字段、類方法陌宿、接口方法锡足、方法類型、方法句柄和調(diào)用點限定符這7類符號引用進行壳坪,分別對應(yīng)于常量池的CONSTANT_Class_info舶得、CON-STANT_Fieldref_info、
CONSTANT_Methodref_info爽蝴、CONSTANT_InterfaceMethodref_info沐批、
CONSTANT_MethodType_info、CONSTANT_MethodHandle_info蝎亚、CONSTANT_Dyna-mic_info和CONSTANT_InvokeDynamic_info 8種常量類型
1.類或接口的解析
假設(shè)當前代碼所處的類為D九孩,如果要把一個從未解析過的符號引用N解析為一個類或接口C的直接引用,那虛擬機完成整個解析的過程需要包括以下3個步驟:
1)如果C不是一個數(shù)組類型发框,那虛擬機將會把代表N的全限定名傳遞給D的類加載器去加載這個類C躺彬。在加載過程中,由于元數(shù)據(jù)驗證梅惯、字節(jié)碼驗證的需要宪拥,又可能觸發(fā)其他相關(guān)類的加載動作,例如加載這個類的父類或?qū)崿F(xiàn)的接口铣减。一旦這個加載過程出現(xiàn)了任何異常她君,解析過程就將宣告失敗。
2)如果C是一個數(shù)組類型葫哗,并且數(shù)組的元素類型為對象缔刹,也就是N的描述符會是類似“[Ljava/lang/Integer”的形式,那將會按照第一點的規(guī)則加載數(shù)組元素類型劣针。如果N的描述符如前面所假設(shè)的形式桨螺,需要加載的元素類是“java.lang.Integer”,接著由虛擬機生成一個代表該數(shù)組維度和元素的數(shù)組對象酿秸。
3)如果上面兩步?jīng)]有出現(xiàn)任何異常灭翔,那么C在虛擬機中實際上已經(jīng)成為一個有效的類或接口了,但在解析完成前還要進行符號引用驗證辣苏,確認D是否具備對C的訪問權(quán)限肝箱。如果發(fā)現(xiàn)不具備訪問權(quán)限,將拋出java.lang.IllegalAccessError異常稀蟋。
針對上面第3點訪問權(quán)限驗證煌张,在JDK 9引入了模塊化以后,一個public類型也不再意味著程序任何位置都有它的訪問權(quán)限退客,我們還必須檢查模塊間的訪問權(quán)限骏融。
如果我們說一個D擁有C的訪問權(quán)限链嘀,那就意味著以下3條規(guī)則中至少有其中一條成立:
- 被訪問類C是public的,并且與訪問類D處于同一個模塊档玻。
- 被訪問類C是public的怀泊,不與訪問類D處于同一個模塊,但是被訪問類C的模塊允許被訪問類D的模塊進行訪問误趴。
- 被訪問類C不是public的霹琼,但是它與訪問類D處于同一個包中。
2.字段解析
要解析一個未被解析過的字段符號引用凉当,首先將會對字段表內(nèi)class_index
項中索引的CONSTANT_Class_info符號引用進行解析枣申,也就是字段所屬的類或接口的符號引用。如果在解析這個類或接口符號引用的過程中出現(xiàn)了任何異常看杭,都會導(dǎo)致字段符號引用解析的失敗忠藤。如果解析成功完成,那把這個字段所屬的類或接口用C表示楼雹,《Java虛擬機規(guī)范》要求按照如下步驟對C進行后續(xù)字段的搜索:
1)如果C本身就包含了簡單名稱和字段描述符都與目標相匹配的字段熄驼,則返回這個字段的直接引用,查找結(jié)束烘豹。
2)否則瓜贾,如果在C中實現(xiàn)了接口,將會按照繼承關(guān)系從下往上遞歸搜索各個接口和它的父接口携悯,如果接口中包含了簡單名稱和字段描述符都與目標相匹配的字段祭芦,則返回這個字段的直接引用,查找結(jié)束憔鬼。
3)否則龟劲,如果C不是java.lang.Object的話,將會按照繼承關(guān)系從下往上遞歸搜索其父類轴或,如果在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段昌跌,則返回這個字段的直接引用,查找結(jié)束照雁。
4)否則蚕愤,查找失敗,拋出java.lang.NoSuchFieldError異常饺蚊。
如果查找過程成功返回了引用萍诱,將會對這個字段進行權(quán)限驗證,如果發(fā)現(xiàn)不具備對字段的訪問權(quán)限污呼,將拋出java.lang.IllegalAccessError異常裕坊。
.方法解析
方法解析的第一個步驟與字段解析一樣,也是需要先解析出方法表的class_index
項中索引的方法所屬的類或接口的符號引用燕酷,如果解析成功籍凝,那么我們依然用C表示這個類周瞎,接下來虛擬機將會按照如下步驟進行后續(xù)的方法搜索:
1)由于Class文件格式中類的方法和接口的方法符號引用的常量類型定義是分開的,如果在類的方法表中發(fā)現(xiàn)class_index中索引的C是個接口的話饵蒂,那就直接拋出java.lang.IncompatibleClassChangeError異常声诸。
2)如果通過了第一步,在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法苹享,如果有則返回這個方法的直接引用,查找結(jié)束浴麻。
3)否則得问,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用软免,查找結(jié)束宫纬。
4)否則,在類C實現(xiàn)的接口列表及它們的父接口之中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法膏萧,如果存在匹配的方法漓骚,說明類C是一個抽象類,這時候查找結(jié)束榛泛,拋出java.lang.AbstractMethodError異常蝌蹂。
5)否則,宣告方法查找失敗曹锨,拋出java.lang.NoSuchMethodError孤个。
最后,如果查找過程成功返回了直接引用沛简,將會對這個方法進行權(quán)限驗證齐鲤,如果發(fā)現(xiàn)不具備對此方法的訪問權(quán)限,將拋出java.lang.IllegalAccessError異常椒楣。
4.接口方法解析(接口方法也有實現(xiàn)给郊?)
接口方法也是需要先解析出接口方法表的class_index項中索引的方法所屬的類或接口的符號引用,如果解析成功捧灰,依然用C表示這個接口淆九,接下來虛擬機將會按照如下步驟進行后續(xù)的接口方法搜索:
1)與類的方法解析相反,如果在接口方法表中發(fā)現(xiàn)class_index中的索引C是個類而不是接口毛俏,那么就直接拋出java.lang.IncompatibleClassChangeError異常吩屹。
2)否則,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法拧抖,如果有則返回這個方法的直接引用煤搜,查找結(jié)束。
3)否則唧席,在接口C的父接口中遞歸查找擦盾,直到j(luò)ava.lang.Object類(接口方法的查找范圍也會包括Object類中的方法)為止嘲驾,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用迹卢,查找結(jié)束辽故。
4)對于規(guī)則3,由于Java的接口允許多重繼承腐碱,如果C的不同父接口中存有多個簡單名稱和描述符都與目標相匹配的方法誊垢,那將會從這多個方法中返回其中一個并結(jié)束查找,《Java虛擬機規(guī)范》中并沒有進一步規(guī)則約束應(yīng)該返回哪一個接口方法症见。
5)否則喂走,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常谋作。
在JDK 9之前芋肠,Java接口中的所有方法都默認是public的,也沒有模塊化的訪問約束遵蚜,所以不存在訪問權(quán)限的問題帖池,接口方法的符號解析就不可能拋出java.lang.IllegalAccessError異常。但在JDK 9中增加了接口的靜態(tài)私有方法吭净,也有了模塊化的訪問約束睡汹,所以從JDK 9起,接口方法的訪問也完全有可能因訪問權(quán)限控制而出現(xiàn)java.lang.IllegalAccessError異常寂殉。
初始化
類的初始化階段是類加載過程的最后一個步驟帮孔,之前介紹的幾個類加載的動作里,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器的方式局部參與外不撑,其余動作都完全由Java虛擬機來主導(dǎo)控制文兢。直到初始化階段,Java虛擬機才真正開始執(zhí)行類中編寫的Java程序代碼焕檬,將主導(dǎo)權(quán)移交給應(yīng)用程序姆坚。
進行準備階段時,變量已經(jīng)賦過一次系統(tǒng)要求的初始零值实愚,而在初始化階段兼呵,則會根據(jù)程序員通過程序編碼制定的主觀計劃去初始化類變量和其他資源。我們也可以從另外一種更直接的形式來表達:初始化階段就是執(zhí)行類構(gòu)造器<clinit>()方法的過程腊敲。<clinit>()并不是程序員在Java代碼中直接編寫的方法击喂,它是Javac編譯器的自動生成物,但我們非常有必要了解這個方法具體是如何產(chǎn)生的碰辅,以及
<clinit>()方法執(zhí)行過程中各種可能會影響程序運行行為的細節(jié)懂昂,這部分比起其他類加載過程更貼近于普通的程序開發(fā)人員的實際工作。
·<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的没宾,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的凌彬,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量沸柔,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值铲敛,但是不能訪問褐澎,如代碼清單7-5所示。
代碼清單7-5 非法前向引用變量
public class Test {
static {
i = 0; // 給變量復(fù)制可以正常編譯通過
System.out.print(i); // 這句編譯器會提示“非法向前引用”
}
static int i = 1;
}
·<clinit>()方法與類的構(gòu)造函數(shù)(即在虛擬機視角中的實例構(gòu)造器<init>()方法)不同伐蒋,它不需要顯式地調(diào)用父類構(gòu)造器工三,Java虛擬機會保證在子類的<clinit>()方法執(zhí)行前,父類的<clinit>()方法已經(jīng)執(zhí)行完畢先鱼。因此在Java虛擬機中第一個被執(zhí)行的<clinit>()方法的類型肯定是java.lang.Object俭正。
·由于父類的<clinit>()方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作型型,如代碼清單7-6中段审,字段B的值將會是2而不是1全蝶。
代碼清單7-6 <clinit>()方法執(zhí)行順序
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>()方法對于類或接口來說并不是必需的闹蒜,如果一個類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作抑淫,那么編譯器可以不為這個類生成<clinit>()方法绷落。
接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作始苇,因此接口與類一樣都會生成<clinit>()方法砌烁。但接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法催式,因為只有當父接口中定義的變量被使用時函喉,父接口才會被初始化。此外荣月,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()方法管呵。
Java虛擬機必須保證一個類的<clinit>()方法在多線程環(huán)境中被正確地加鎖同步,如果多個線程同時去初始化一個類哺窄,那么只會有其中一個線程去執(zhí)行這個類的<clinit>()方法捐下,其他線程都需要阻塞等待,直到活動線程執(zhí)行完畢<clinit>()方法萌业。如果在一個類的<clinit>()方法中有耗時很長的操作坷襟,那就可能造成多個線程阻塞,在實際應(yīng)用中這種阻塞往往是很隱蔽的生年。代碼清單7-7演示了這種場景婴程。
package jvm.class_test;
public class DeadLoopClassTest {
public static void main(String[] args) {
Runnable script = new Runnable() {
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + " run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
class DeadLoopClass {
static {
// 如果不加上這個if語句,編譯器將提示“Initializer does not complete normally”
// 并拒絕編譯
if (true) {
System.out.println(Thread.currentThread() + "init DeadLoopClass");
while (true) {
}
}
}
}
運行結(jié)果如下抱婉,一條線程在死循環(huán)以模擬長時間操作排抬,另外一條線程在阻塞等待:
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]init DeadLoopClass
類加載器
Java虛擬機設(shè)計團隊有意把類加載階段中的“通過一個類的全限定名來獲取描述該類的二進制字節(jié)流”這個動作放到Java虛擬機外部去實現(xiàn)懂从,以便讓應(yīng)用程序自己決定如何去獲取所需的類。實現(xiàn)這個動作的代碼被稱為“類加載器”(Class Loader)蹲蒲。
類加載器可以說是Java語言的一項創(chuàng)新番甩,它是早期Java語言能夠快速流行的重要原因之一。類加載器最初是為了滿足Java Applet的需求而設(shè)計出來的届搁,在今天用在瀏覽器上的Java Applet技術(shù)基本上已經(jīng)被淘汰缘薛,但類加載器卻在類層次劃分、OSGi卡睦、程序熱部署宴胧、代碼加密等領(lǐng)域大放異彩,成為Java技術(shù)體系中一塊重要的基石表锻,可謂是失之桑榆恕齐,收之東隅。
類與類加載器
類加載器雖然只用于實現(xiàn)類的加載動作瞬逊,但它在Java程序中起到的作用卻遠超類加載階段显歧。對于任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機中的唯一性确镊,每一個類加載器士骤,都擁有一個獨立的類名稱空間。這句話可以表達得更通俗一些:比較兩個類是否“相等”蕾域,只有在這兩個類是由同一個類加載器加載的前提下才有意義拷肌,否則,即使這兩個類來源于同一個
Class文件旨巷,被同一個Java虛擬機加載巨缘,只要加載它們的類加載器不同,那這兩個類就必定不相等采呐。
這里所指的“相等”若锁,包括代表類的Class對象的equals()方法、isAssignableFrom()方法懈万、isInstance()方法的返回結(jié)果拴清,也包括了使用instanceof關(guān)鍵字做對象所屬關(guān)系判定等各種情況。如果沒有注意到類加載器的影響会通,在某些情況下可能會產(chǎn)生具有迷惑性的結(jié)果口予,代碼清單7-8中演示了不同的類加載器對instanceof關(guān)鍵字運算的結(jié)果的影響。
代碼清單7-8 不同的類加載器對instanceof關(guān)鍵字運算的結(jié)果的影響
package jvm.classloader;
import java.io.IOException;
import java.io.InputStream;
/**
* 類加載器與instanceof關(guān)鍵字演示
*
* @author zzm
*/
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
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("jvm.classloader.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println("class loader" + obj.getClass().getClassLoader());
System.out.println(obj instanceof jvm.classloader.ClassLoaderTest);
System.out.println(jvm.classloader.ClassLoaderTest.class.getClassLoader());
}
}
class jvm.classloader.ClassLoaderTest
class loaderjvm.classloader.ClassLoaderTest$1@4e50df2e
false
sun.misc.Launcher$AppClassLoader@18b4aac2
代碼清單7-8中構(gòu)造了一個簡單的類加載器涕侈,盡管它極為簡陋沪停,但是對于這個演示來說已經(jīng)足夠。它可以加載與自己在同一路徑下的Class文件,我們使用這個類加載器去加載了一個名為“jvm.classloader.ClassLoaderTest”的類木张,并實例化了這個類的對象众辨。
兩行輸出結(jié)果中,從第一行可以看到這個對象確實是類jvm.classloader.ClassLoaderTest實例化出來的舷礼,但在第二行的輸出中卻發(fā)現(xiàn)這個對象與類jvm.classloader.ClassLoaderTest做所屬類型檢查的時候返回了false鹃彻。這是因為Java虛擬機中同時存在了兩個ClassLoaderTest類,一個是由虛擬機的應(yīng)用程序類加載器所加載的妻献,另外一個是由我們自定義的類加載器加載的蛛株,雖然它們都來自同一個Class文件,但在Java虛擬機中仍然是兩個互相獨立的類育拨,做對象所屬類型檢查時的結(jié)果自然為false谨履。
雙親委派模型
站在Java虛擬機的角度來看,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader)熬丧,這個類加載器使用C++語言實現(xiàn)[1]笋粟,是虛擬機自身的一部分;另外一種就是其他所有的類加載器析蝴,這些類加載器都由Java語言實現(xiàn)害捕,獨立存在于虛擬機外部,并且全都繼承自抽象類java.lang.ClassLoader嫌变。
站在Java開發(fā)人員的角度來看吨艇,類加載器就應(yīng)當劃分得更細致一些躬它。自JDK 1.2以來腾啥,Java一直保持著三層類加載器、雙親委派的類加載架構(gòu)冯吓,盡管這套架構(gòu)在Java模塊化系統(tǒng)出現(xiàn)后有了一些調(diào)整變動倘待,但依然未改變其主體結(jié)構(gòu),
本節(jié)內(nèi)容將針對JDK 8及之前版本的Java來介紹什么是三層類加載器组贺,以及什么是雙親委派模型凸舵。對于這個時期的Java應(yīng)用,絕大多數(shù)Java程序都會使用到以下3個系統(tǒng)提供的類加載器來進行加載失尖。
·啟動類加載器(Bootstrap Class Loader):前面已經(jīng)介紹過啊奄,這個類加載器負責(zé)加載存放在<JAVA_HOME>\lib目錄,或者被-Xbootclasspath參數(shù)所指定的路徑中存放的掀潮,而且是Java虛擬機能夠識別的(按照文件名識別菇夸,如rt.jar、tools.jar仪吧,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機的內(nèi)存中庄新。啟動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導(dǎo)類加載器去處理择诈,那直接使用null代替即可械蹋。
·擴展類加載器(Extension Class Loader):這個類加載器是在類sun.misc.Launcher$ExtClassLoader中以Java代碼的形式實現(xiàn)的。它負責(zé)加載<JAVA_HOME>\lib\ext目錄中羞芍,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中所有的類庫哗戈。根據(jù)“擴展類加載器”這個名稱,就可以推斷出這是一種Java系統(tǒng)類庫的擴展機制荷科,JDK的開發(fā)團隊允許用戶將具有通用性的類庫放置在ext目錄里以擴展Java SE的功能谱醇,在JDK9之后,這種擴展機制被模塊化帶來的天然的擴展能力所取代步做。由于擴展類加載器是由Java代碼實現(xiàn)的副渴,開發(fā)者可以直接在程序中使用擴展類加載器來加載Class文件。
-
合中也稱它為“系統(tǒng)類加載器”全度。它負責(zé)加載用戶類路徑
(ClassPath)上所有的類庫煮剧,開發(fā)者同樣可以直接在代碼中使用這個類加載器。如果應(yīng)用程序中沒有自定義過自己的類加載器将鸵,一般情況下這個就是程序中默認的類加載器勉盅。
JDK 9之前的Java應(yīng)用都是由這三種類加載器互相配合來完成加載的,如果用戶認為有必要顶掉,還可以加入自定義的類加載器來進行拓展草娜。
圖7-2中展示的各種類加載器之間的層次關(guān)系被稱為類加載器的“雙親委派模型(Parents Delegation Model)”。雙親委派模型要求除了頂層的啟動類加載器外痒筒,其余的類加載器都應(yīng)有自己的父類加載器宰闰。不過這里類加載器之間的父子關(guān)系一般不是以繼承(Inheritance)的關(guān)系來實現(xiàn)的,而是通常使用組合(Composition)關(guān)系來復(fù)用父加載器的代碼簿透。
雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求移袍,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成老充,每一個層次的類加載器都是如此葡盗,因此所有的加載請求最終都應(yīng)該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去完成加載。
使用雙親委派模型來組織類加載器之間的關(guān)系堰汉,一個顯而易見的好處就是Java中的類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。例如類java.lang.Object喘先,它存放在rt.jar之中,無論哪一個類加載器要加載這個類涂籽,最終都是委派給處于模型最頂端的啟動類加載器進行加載苹祟,因此Object類在程序的各種類加載器環(huán)境中都能夠保證是同一個類。反之,如果沒有使用雙親委派模型树枫,都由各個類加載器自行去加載的話直焙,如果用戶自己也編寫了一個名為java.lang.Object的類,并放在程序的ClassPath中砂轻,那系統(tǒng)中就會出現(xiàn)多個不同的Object類奔誓,Java類型體系中最基礎(chǔ)的行為也就無從保證,應(yīng)用程序?qū)兊靡黄靵y搔涝。如果大家有興趣的話厨喂,可以嘗試去寫一個與rt.jar類庫中已有類重名的Java類,將會發(fā)現(xiàn)它可以正常編譯庄呈,但永遠無法被加載運行(即使自定義了自己的類加載器蜕煌,強行用defineClass()方法去加載一個以“java.lang”開頭的類也不會成功。如果讀者嘗試這樣做的話诬留,將會收到一個由Java虛擬機內(nèi)部拋出的“java.lang.SecurityException:Prohibited package name:java.lang”異常斜纪。)
雙親委派模型對于保證Java程序的穩(wěn)定運作極為重要,但它的實現(xiàn)卻異常簡單文兑,用以實現(xiàn)雙親委派的代碼只有短短十余行盒刚,全部集中在java.lang.ClassLoader的loadClass()方法之中,如代碼清單7-10所示绿贞。
代碼清單7-10 雙親委派模型的實現(xiàn)
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
// 首先因块,檢查請求的類是否已經(jīng)被加載過了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父類加載器拋出ClassNotFoundException
// 說明父類加載器無法完成加載請求
}
if (c == null) {
// 在父類加載器無法加載時
// 再調(diào)用本身的findClass方法來進行類加載
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
這段代碼的邏輯清晰易懂:先檢查請求加載的類型是否已經(jīng)被加載過,若沒有則調(diào)用父加載器的loadClass()方法籍铁,若父加載器為空則默認使用啟動類加載器作為父加載器涡上。假如父類加載器加載失敗,拋出ClassNotFoundException異常的話寨辩,才調(diào)用自己的findClass()方法嘗試進行加載吓懈。
破壞雙親委派模型
上文提到過雙親委派模型并不是一個具有強制性約束的模型歼冰,而是Java設(shè)計者推薦給開發(fā)者們的類加載器實現(xiàn)方式靡狞。在Java的世界中大部分的類加載器都遵循這個模型,但也有例外的情況隔嫡,直到Java模塊化出現(xiàn)為止甸怕,雙親委派模型主要出現(xiàn)過3次較大規(guī)模“被破壞”的情況腮恩。
OSGi實現(xiàn)模塊化熱部署的關(guān)鍵是它自定義的類加載器機制的實現(xiàn)梢杭,每一個程序模塊(OSGi中稱為Bundle)都有一個自己的類加載器,當需要更換一個Bundle時秸滴,就把Bundle連同類加載器一起換掉以實現(xiàn)代碼的熱替換武契。在OSGi環(huán)境下,類加載器不再雙親委派模型推薦的樹狀結(jié)構(gòu),而是進一步發(fā)展為更加復(fù)雜的網(wǎng)狀結(jié)構(gòu)咒唆,當收到類加載請求時届垫,OSGi將按照下面的順序進行類搜索:
1)將以java.*開頭的類,委派給父類加載器加載全释。
2)否則装处,將委派列表名單內(nèi)的類,委派給父類加載器加載浸船。
3)否則妄迁,將Import列表中的類,委派給Export這個類的Bundle的類加載器加載李命。
4)否則登淘,查找當前Bundle的ClassPath,使用自己的類加載器加載封字。
5)否則形帮,查找類是否在自己的Fragment Bundle中,如果在周叮,則委派給Fragment Bundle的類加載器
加載辩撑。
6)否則,查找Dynamic Import列表的Bundle仿耽,委派給對應(yīng)Bundle的類加載器加載合冀。
7)否則,類查找失敗项贺。
上面的查找順序中只有開頭兩點仍然符合雙親委派模型的原則君躺,其余的類查找都是在平級的類加載器中進行的.