概述
深入了解了Class文件存儲(chǔ)格式的具體細(xì)節(jié)后祟偷,虛擬機(jī)如何加載這些Class文件属铁?Class文件中的信息進(jìn)入虛擬機(jī)后會(huì)發(fā)生什么變化?這是作者第七章講解的內(nèi)容。
虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化恶座,最終形成可以被虛擬機(jī)直接使用的 Java 類型,這就是虛擬機(jī)的類加載機(jī)制沥阳。
類加載都是在程序運(yùn)行期間完成的跨琳,雖然會(huì)增加程序一點(diǎn)性能開銷,但能為 Java 應(yīng)用提供高度的靈活性桐罕。通過依賴運(yùn)行期動(dòng)態(tài)加載和動(dòng)態(tài)連接特點(diǎn)使 Java 具備動(dòng)態(tài)擴(kuò)展的語言特性脉让。例如:
- 編寫面向接口的應(yīng)用程序,可以等到運(yùn)行時(shí)再指定其實(shí)際的實(shí)現(xiàn)類
- 通過 Java 預(yù)定義的和自定義類加載器在運(yùn)行時(shí)從其他地方加載二進(jìn)制流作為程序代買的一部分(Applet功炮、JSP溅潜、OSGi 技術(shù))
類加載的時(shí)機(jī)
類的生命周期包括:加載(Loading)、驗(yàn)證(Verification)薪伏、準(zhǔn)備(Preparation)滚澜、解析(Resolution)、初始化(Initialization)嫁怀、使用(Using)和卸載(Unloading)共 7 個(gè)階段设捐。
其中驗(yàn)證、準(zhǔn)備塘淑、解析 3 個(gè)部分統(tǒng)稱為連接(Linking)萝招,這 7 個(gè)階段的發(fā)生順序如下圖所示:
什么時(shí)候開始類加載過程的第一個(gè)階段:加載?
Java 虛擬機(jī)規(guī)范并沒有強(qiáng)制規(guī)定加載(Loading)的時(shí)機(jī)朴爬。但嚴(yán)格規(guī)定有且只有在以下 5 種情況時(shí)如果類沒有初始化即寒,則需要先觸發(fā)其初始化(Initialization)。
初始化之前,自然會(huì)進(jìn)行加載和連接母赵。
- 遇到 new(實(shí)例化對(duì)象)逸爵、getstatic(讀取除常量外靜態(tài)字段)、putstatic(設(shè)置讀取除常量外靜態(tài)字段) 或 invokestatic(調(diào)用類的靜態(tài)方法) 這 4 條字節(jié)碼指令時(shí)凹嘲,所在的類需要初始化师倔。
- 使用 java.lang.reflect 包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)。
- 初始化一個(gè)類時(shí)周蹭,如果其父類沒有初始化趋艘,則需先初始化其父類(接口除外,只有使用到父接口的時(shí)候才會(huì)初始化)凶朗。
- 虛擬機(jī)啟動(dòng)時(shí)會(huì)先初始化用戶指定的執(zhí)行主類(包含 main 方法的類)瓷胧。
- 使用 JDK1.7 動(dòng)態(tài)語言支持時(shí),java.lang.invoke.MethodHandle 實(shí)例最后解析的結(jié)果為 REF_getStatic棚愤、REF_putStatic搓萧、REF_invokeStatic 的方法句柄時(shí),則這個(gè)方法句柄對(duì)應(yīng)的類需要初始化宛畦。
上述 5 種場(chǎng)景中的行為成為對(duì)一個(gè)類主動(dòng)引用瘸洛。除了主動(dòng)引用之外,所有引用類的方式都不會(huì)觸發(fā)類的初始化次和,稱為被動(dòng)引用反肋。
類加載的過程
接下來講解加載、驗(yàn)證踏施、準(zhǔn)備石蔗、解析和初始化這 5 個(gè)階段所執(zhí)行的具體動(dòng)作。
加載
加載是類加載過程第一個(gè)階段读规。在加載階段虛擬機(jī)要做 3 件事:
- 通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制流抓督。
- 可以從壓縮包中讀取,如:JAR束亏、EAR铃在、WAR 格式。
- 從網(wǎng)絡(luò)讀取碍遍,如:Applet 定铜。
- 運(yùn)行時(shí)計(jì)算生成,如:動(dòng)態(tài)代理技術(shù)在 java.lang.reflect.Proxy 中怕敬,通過 ProxyGenerator.generateProxyClass為特定接口生成形式為「*$Proxy」的代理類的二進(jìn)制字節(jié)流揣炕。
- 由其他文件生成,如:JSP 應(yīng)用通過 JSP 文件生成對(duì)應(yīng)的 Class 類东跪。
- 從數(shù)據(jù)庫中讀取畸陡,如:中間件服務(wù)器 SAP Netweaver 可以選擇把程序安裝到數(shù)據(jù)庫中來完成程序代碼在集群間分發(fā)鹰溜。
……
- 將這個(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ù)的訪問入口曹动。
獲取類的二進(jìn)制流是開發(fā)人員可控性最強(qiáng)的,既可以通過系統(tǒng)提供的啟動(dòng)類加載器完成牲览,也可以由用戶自定義類加載器來控制字節(jié)流的獲取方式(重寫類加載器的 loadClass 方法)墓陈。
數(shù)組類比較特殊,它不通過類加載器創(chuàng)建第献,而是由 Java 虛擬機(jī)直接創(chuàng)建贡必。但數(shù)組的元素類型(Element Type)最終是要靠類加載器創(chuàng)建。
加載階段完成后庸毫,虛擬機(jī)外部的二進(jìn)制字節(jié)流就會(huì)按照所需的格式存儲(chǔ)在方法區(qū)中仔拟,存儲(chǔ)格式由虛擬機(jī)自行定義。然后在內(nèi)存中實(shí)例化一個(gè) java.lang.Class 對(duì)象(并沒有在堆中飒赃,Class 對(duì)象雖然是對(duì)象理逊,但在 HotSpot虛擬機(jī)中是存放在方法區(qū)里),這個(gè)對(duì)象將作為程序訪問方法區(qū)中這些類型數(shù)據(jù)的外部接口盒揉。
加載階段與連接階段的驗(yàn)證中一部分字節(jié)碼文件格式驗(yàn)證動(dòng)作是交叉進(jìn)行的,加載階段尚未完成兑徘,連接階段就可能已經(jīng)開始刚盈。這兩個(gè)階段總體的開始時(shí)間仍然保持固定的先后順序。
驗(yàn)證
驗(yàn)證階段的目的是保證 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求挂脑,并不會(huì)危害虛擬機(jī)的安全藕漱。
如加載階段所述,Class 文件并不一定是 Java 源碼編譯而來崭闲,甚至可以用 16 進(jìn)制編輯器直接編寫肋联。虛擬機(jī)如果不進(jìn)行字節(jié)流驗(yàn)證,可能因載入有害字節(jié)流而導(dǎo)致系統(tǒng)崩潰刁俭。
驗(yàn)證階段大致上會(huì)完成 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)前版本的虛擬機(jī)處理∪缧ⅲ可能包括以下驗(yàn)證點(diǎn):
- 是否魔數(shù)以 0xCAFEBABE 開頭宪哩。
- 主、次版本號(hào)是否在當(dāng)前虛擬機(jī)的處理范圍內(nèi)第晰。
- 常量池中是否有不支持的常量類型(檢查常量 tag 標(biāo)志)锁孟。
- CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數(shù)據(jù)彬祖。
- Class 文件中各個(gè)部分及文件本身是否有被刪除或附加的其他信息
……
此階段的驗(yàn)證是基于二進(jìn)制字節(jié)流,只有通過了這個(gè)階段后品抽,字節(jié)流才會(huì)進(jìn)入方法區(qū)內(nèi)進(jìn)行存儲(chǔ)储笑。后面的 3 個(gè)階段驗(yàn)證全都是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不會(huì)再操作字節(jié)流桑包。
元數(shù)據(jù)驗(yàn)證
對(duì)字節(jié)碼描述信息進(jìn)行語義分析南蓬,確保其描述信息符合 Java 語言規(guī)范的要求⊙屏耍可能包括以下驗(yàn)證點(diǎn):
- 是否有父類(除了 java.lang.Object 之外所有類都有父類)赘方。
- 父類是否繼承了不允許被繼承的類(被 final 修飾的類)。
- 如果不是抽象類弱左,是否實(shí)現(xiàn)了父類或接口中要求實(shí)現(xiàn)的方法窄陡。
- 類中的字段、方法是否與父類產(chǎn)生矛盾(如覆蓋了父類的 final 字段拆火,或出現(xiàn)不符合規(guī)則的方法重載跳夭,例如方法參數(shù)一樣,但返回值不同等)
……
字節(jié)碼驗(yàn)證
驗(yàn)證階段最復(fù)雜的階段们镜,主要目的是通過數(shù)據(jù)流和控制流分析币叹,確定程序的語義是合法的、符合邏輯的模狭。在對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗(yàn)后對(duì)方法體進(jìn)行校驗(yàn)分析颈抚,保證在運(yùn)行時(shí)方法不會(huì)做出危害虛擬機(jī)安全的事件。
由于數(shù)據(jù)流驗(yàn)證的高復(fù)雜性嚼鹉,為避免過多的時(shí)間消耗贩汉,JDK 1.6 以后 Javac 編譯器和 Java虛擬機(jī)進(jìn)行了一項(xiàng)優(yōu)化,給方法體的 Code 屬性的屬性表增加了一項(xiàng)名為 StackMapTable 屬性锚赤。這個(gè)屬性描述了方法體中所有的基本塊(按控制流拆分的代碼塊)開始時(shí)本地變量表和操作數(shù)棧應(yīng)有的狀態(tài)匹舞。在字節(jié)碼驗(yàn)證階段就不需要根據(jù)程序推導(dǎo)狀態(tài)合法性,只要檢查 StackMapTable 屬性中的記錄是否合法即可线脚。
理論上 StackMapTable 屬性也存在被篡改的可能赐稽。有可能在惡意篡改 Code 屬性的同事生成相應(yīng)的 StackMapTable屬性來騙過虛擬機(jī)類型校驗(yàn)。
符號(hào)引用驗(yàn)證
驗(yàn)證目的是確保符號(hào)引用轉(zhuǎn)直接引用在解析階段能正常執(zhí)行酒贬。符號(hào)引用驗(yàn)證可以看做是對(duì)類自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn)又憨。通常需要校驗(yàn)一下內(nèi)容:
- 符號(hào)引用中通過字符串描述的全限定名是否能找到對(duì)應(yīng)的類。
- 在指定類中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱所描述的方法和字段锭吨。
- 符號(hào)引用中農(nóng)的類蠢莺、字段、方法的訪問性是否可被當(dāng)前類訪問
……
如果無法通過符號(hào)引用驗(yàn)證零如,將會(huì)拋出 java.lang.IncompatibleClassChangeError 異常的子類躏将,如 java.lang.IllegalAccessError锄弱、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等祸憋。
雖然驗(yàn)證階段十分重要会宪,但如果能保證自己編寫的以及第三方包中代碼都已經(jīng)反復(fù)使用和驗(yàn)證過,可以使用-Xverify:none
參數(shù)來關(guān)閉大部分的驗(yàn)證措施蚯窥,以縮短加載時(shí)間掸鹅。
準(zhǔn)備
準(zhǔn)備階段是正式為類變量(除常量外,被 static 修飾的變量)在方法區(qū)分配內(nèi)存并設(shè)置類變量的初始值(數(shù)字類型為 0拦赠,布爾類型為 false巍沙,引用類型為 null……)階段。實(shí)例變量在準(zhǔn)備階段是不會(huì)設(shè)值的荷鼠,而是在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在 Java
堆中句携。
例如在下面的代碼中,類變量 value 在準(zhǔn)備階段后值為 0 而不是 123允乐。因?yàn)榇藭r(shí)尚未執(zhí)行任何 Java 方法矮嫉,把 123 賦值給 value 的 putstatic 指令是被程序編譯后存放在類構(gòu)造器 <client>() 方法中的,這個(gè)方法只有在初始化階段才會(huì)執(zhí)行牍疏。
public static int value = 123;
如果這個(gè)變量是常量蠢笋,類字段的字段屬性表中存在 ConstantValue 屬性,那么準(zhǔn)備階段變量 value 就會(huì)被初始化為 ConstantValue 屬性所指定的值鳞陨。例如:
public final static int value = 123;
編譯時(shí) Javac 將會(huì)為 value 生成 ConstantValue 屬性挺尿,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù) ConstantValue 值把 value 賦值為 123。
解析
解析階段是將虛擬機(jī)常量池內(nèi)的符號(hào)引用替換為直接引用的過程炊邦。
符號(hào)引用(Symbolic Reference):以一組符號(hào)來描述引用的目標(biāo),符號(hào)的字面量形式明確的定義在 Java 虛擬機(jī)規(guī)范中的 Class 文件格式中熟史。
直接引用(Direct Reference):直接引用可以是直接指向目標(biāo)的指針馁害、相對(duì)偏移量或者是一個(gè)間接能定位到目標(biāo)的句柄。如果有了直接引用蹂匹,那目標(biāo)一定存在于內(nèi)存中碘菜。
虛擬機(jī)規(guī)范沒有規(guī)定解析階段發(fā)生的具體時(shí)間,可以自行決定到底在類被加載器加載時(shí)就對(duì)常量池中的符號(hào)要引用進(jìn)行解析限寞,還是等到一個(gè)符號(hào)引用將要被使用前才去解析他忍啸。
解析動(dòng)作主要針對(duì)類或接口、字段履植、類方法计雌、接口方法、方法類型玫霎、方法句柄和調(diào)用點(diǎn)限定符 7 類符號(hào)引用進(jìn)行凿滤。后 3 種符號(hào)引用與 JDK 1.7 新增的動(dòng)態(tài)語言支持息息相關(guān)妈橄。
類或接口的解析
完成類或接口中的符號(hào)引用解析,在對(duì)符號(hào)引用的類加載過程中可能觸發(fā)相關(guān)類的加載動(dòng)作翁脆,所有類加載完成后如果沒有異常眷蚓,還需要進(jìn)行符號(hào)引用的訪問權(quán)限驗(yàn)證。
字段解析
解析字段符號(hào)引用首先會(huì)解析字段所屬的類或接口的符號(hào)引用反番,如果沒有異常沙热,虛擬機(jī)規(guī)范要求按如下步驟進(jìn)行搜索:
- 如果所屬類本身就包含簡(jiǎn)單名稱和字段描述符與該字段匹配的字段,結(jié)束并返回這個(gè)字段罢缸。
- 否則篙贸,按照繼承關(guān)系從下往上遞歸搜索各個(gè)父接口,如果包含了匹配字段祖能,結(jié)束返回歉秫。
- 否則,按繼承關(guān)系從下往上遞歸搜索父類养铸,如果包含了匹配字段雁芙,結(jié)束并返回。
- 否則钞螟,查找失敗拋出 java.lang.NoSuchFieldError 異常兔甘。
查找完成后會(huì)對(duì)字段進(jìn)行訪問權(quán)限驗(yàn)證,沒有權(quán)限時(shí)拋出 java.lang.IllegalAccessError 異常鳞滨。
類方法解析
類方法解析與字段解析第一步一樣洞焙,先解析類方法所在的類或接口的符號(hào)引用,如果沒有異常拯啦,虛擬機(jī)規(guī)范要求按如下步驟進(jìn)行搜索:
- 類方法與接口方法的常量類型定義不同澡匪,如果發(fā)現(xiàn)類方法表中 class_index 中索引的是接口方法,直接拋出 java.lang.IncompatibleClassChangeError 異常褒链。
- 如果在所在類中找到與目標(biāo)簡(jiǎn)單名稱和描述符相同的方法(以下簡(jiǎn)稱「匹配」)唁情,直接返回這個(gè)方法的直接引用,查找結(jié)束甫匹。
- 否則甸鸟,在類的父類中遞歸查找是否有匹配的方法,如果有返回方法的直接引用兵迅,查找結(jié)束抢韭。
- 否則,在類實(shí)現(xiàn)的接口列表中及他們的父接口中遞歸查找是否有匹配的方法恍箭,如果有刻恭,證明此類是個(gè)抽象類,查找結(jié)束扯夭,拋出 java.lang.AbastractMethodError 異常吠各。
- 否則臀突,宣布查找方法失敗,拋出 java.lang.NoSuchMethodError 異常贾漏。
最后對(duì)類方法進(jìn)行訪問權(quán)限驗(yàn)證候学,沒有權(quán)限時(shí)拋出 java.lang.IllegalAccessError 異常。
接口方法解析
與類方法解析第一步一樣纵散,先解析類方法所屬類或接口的符號(hào)引用梳码。如果解析成功,虛擬機(jī)規(guī)范要求按如下步驟進(jìn)行搜索:
- 與類方法解析不同伍掀,如果在接口方法表中發(fā)現(xiàn) class_index 中索引的是類方法掰茶,那就直接拋出 java.lang.IncompatibleClassChangeError 異常。
- 否則蜜笤,在接口中查找是否存在匹配的方法濒蒋,如果存在直接返回接口方法的直接引用,查找結(jié)束把兔。
- 否則沪伙,在接口的父接口中遞歸查找,直到 java.lang.Object 類(查找范圍可能會(huì)包括 Object 類)為止县好,搜索是否有匹配的方法围橡,如果有返回這個(gè)接口發(fā)方法的直接引用,查找結(jié)束缕贡。
- 否則翁授,宣告查找失敗,拋出 java.lang.NoSuchMethodError 異常晾咪。
由于接口方法都是 public 修飾的收擦,因此不需要進(jìn)行訪問權(quán)限判斷。
初始化
在整個(gè)類加載的過程中谍倦,除了加載階段用戶應(yīng)用程序可以自定義類加載器進(jìn)行控制炬守,其余的階段都是有虛擬機(jī)主導(dǎo)完成的。到了初始化階段才真正開始執(zhí)行類中定義的 Java 程序代碼(或者說是字節(jié)碼)剂跟。
在準(zhǔn)備階段類變量已經(jīng)賦過一次初始值。在初始化階段虛擬機(jī)會(huì)根據(jù)程序員的代碼去初始化類變量和其他資源酣藻〔芮ⅲ或者說初始化的過程是執(zhí)行 <clinit>() 方法的過程。
<clinit>() 方法是由編譯器按照源文件中出現(xiàn)的順序辽剧,自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語句塊中的語句合并而成的送淆。靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在他之后的變量只能賦值不能讀取怕轿。
// 非法向前引用變量代碼示例
public Class Test{
static {
i = 0; //靜態(tài)塊中給后面的類變量賦值可以通過編譯
System.out.println(i);//靜態(tài)塊中讀取定義在后面的類變量編譯器會(huì)提示「非法向前引用」
}
static int i = 1;
}
<clinit>() 方法和類的構(gòu)造函數(shù)(<init>() 方法)不同偷崩,不需要顯示地調(diào)用父類構(gòu)造器辟拷,虛擬機(jī)會(huì)保證子類的 <clinit>() 方法執(zhí)行前父類的該方法已經(jīng)執(zhí)行完畢。因此阐斜,虛擬機(jī)中第一個(gè)執(zhí)行的 <clinit>() 方法一定是 java.lang.Object 類衫冻。
由于父類的 <clinit>() 方法先執(zhí)行,則父類定義的靜態(tài)代碼塊先于子類的變量賦值操作谒出。
如果一個(gè)類中沒有靜態(tài)語句塊隅俘,也沒有對(duì)類變量的賦值操作,那么編譯器可以不為這個(gè)類生成 <clinit>() 方法笤喳。
虛擬機(jī)會(huì)保證一個(gè)類的 <clinit>() 方法在多線程環(huán)境中被正確的加鎖为居、同步,如果有多個(gè)線程去初始化同一個(gè)類杀狡,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的 <clinit>() 方法蒙畴,其他線程都需要阻塞等待。如果一個(gè)類的 <clinit>() 方法中存在耗時(shí)很長(zhǎng)的操作呜象,很可能造成多線程阻塞膳凝。同一個(gè)類加載器只會(huì)加載一次類,因此多線程下只會(huì)執(zhí)行一次 <clinit>() 方法董朝。
類加載器
在虛擬機(jī)外部鸠项,把實(shí)現(xiàn)通過一個(gè)類的全限定名來獲取這個(gè)類的二進(jìn)制字節(jié)流的動(dòng)作的代碼模塊成為類加載器。類加載器在類層次劃分子姜、OSGi祟绊、熱部署、代碼加密等領(lǐng)域大放異彩哥捕,成為 Java 技術(shù)體系中的重要基石牧抽。
類與類加載器
在 Java 虛擬機(jī)中,任一一個(gè)類的唯一性是由該類與其類加載器共同確立的遥赚。也就是說扬舒,同一個(gè)類在同一個(gè)虛擬機(jī)中,但在不同的類加載器中凫佛,那這兩個(gè)類則不相等讲坎。「相等」的判斷依據(jù)是 Class 對(duì)象的 equals() 方法愧薛、isAssignableFrom() 方法晨炕、isInstance() 方法的返回結(jié)果,也包括 instanceof 關(guān)鍵字對(duì)對(duì)象所屬關(guān)系的判定等情況毫炉。在使用自定義類加載器時(shí)需要注意這點(diǎn)瓮栗。
雙親委派模型
從 Java 開發(fā)人員的角度看,系統(tǒng)提供的類加載器可劃分為以下 3 種:
- 啟動(dòng)類加載器(Bootstrap ClassLoader):這個(gè)類負(fù)責(zé)將存放在 <JAVA_HOME>\lib 目錄中的或者被 -Xbootclasspath 參數(shù)指定的路徑中指定名稱(例如 rt.jar)的類庫。啟動(dòng)類加載器無法被 Java 程序直接引用费奸,在自定義類加載器中弥激,如果需要啟動(dòng)類加載器來加載類,在需要傳入 ClassLoader 做參數(shù)的方法中直接把 null 作為程序的類加載器代替即可愿阐。例如微服,在如下方法的第 3 個(gè)參數(shù)傳 null 即可。
public static Class<?> forName(String name, boolean initialize,ClassLoader loader) throws ClassNotFoundException{
…
}
- 擴(kuò)展類加載器(Extension ClassLoader):這個(gè)加載器由 sun.misc.Launcher$ExtClassLoader 實(shí)現(xiàn)换况,負(fù)責(zé)加載 <JAVA_HOME>\lib\ext 目錄中的职辨,或者被 java.ext.dirs 系統(tǒng)變量所指定的路徑中的所有類庫,開發(fā)者可以直接使用擴(kuò)展類加載器戈二。
- 應(yīng)用程序類加載器(Application ClassLoader):這個(gè)類加載器由 sun.misc.Launcher$AppClassLoader 實(shí)現(xiàn)舒裤。這個(gè)類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也成為系統(tǒng)類加載器觉吭。負(fù)責(zé)加載用戶類路徑(classpath)上所指定的類庫腾供,開發(fā)者可以直接使用。如果應(yīng)用程序中沒有指定類加載器一般就作為默認(rèn)類加載器鲜滩。
此外伴鳖,我們也可以自己定義的類加載器。這些類加載器關(guān)系一般如下圖所示:
雙親委派模型并不是強(qiáng)制性的約束模型徙硅,而是一種 Java 設(shè)計(jì)者推薦的類加載器實(shí)現(xiàn)方式榜聂。雙親委派模型工作過程是:當(dāng)某個(gè)類加載器收到加載類請(qǐng)求,首先會(huì)把這個(gè)請(qǐng)求委派給父類加載器去完成嗓蘑,每一層都如此须肆,直到啟動(dòng)類加載器中,只有當(dāng)父類加載器反饋無法加載時(shí)才會(huì)交由子類加載器去加載桩皿。
破壞雙親委派模型
- JNDI 服務(wù)需要加載 SPI 提供的代碼
雙親委派模型很好的解決了各個(gè)類加載器的基礎(chǔ)類的統(tǒng)一問題(基礎(chǔ)類都是上層類加載器加載的)豌汇,但如果基礎(chǔ)類想要調(diào)用用戶代碼就無法實(shí)現(xiàn)。典型的場(chǎng)景是 JNDI 服務(wù)(Java Naming and Directory Interface泄隔,Java 命名和目錄接口)拒贱,JNDI 的目的是對(duì)資源進(jìn)行集中管理和查找,它需要由獨(dú)立廠商實(shí)現(xiàn)并部署在應(yīng)用程序的 Classpath 下的 JNDI 接口提供者(SPI佛嬉,Service Provider Interface)的代碼逻澳。但啟動(dòng)類加載器不能加載這些代碼!怎么辦暖呕?
Java 通過線程上下文類加載(Thread Context ClassLoader)這個(gè)類加載器斜做,可以再 java.lang.Thread 類的 setContextClassLoader() 方法進(jìn)行設(shè)置,如果線程沒有設(shè)置則從其父線程中繼承一個(gè)缰揪,如果應(yīng)用程序沒有全局都沒有設(shè)置過,則這個(gè)類加載器默認(rèn)就是應(yīng)用程序類加載器。
JNDI 服務(wù)可以使用線程上下文類加載 SPI 代碼钝腺,也就是父類加載器請(qǐng)求子類加載器去完成類加載動(dòng)作抛姑,這打破了雙親委派模型的類層次結(jié)構(gòu)。Java 中所有涉及 SPI 的加載動(dòng)作基本都采用了這種方式艳狐,例如 JNDI定硝、JDBC、JCE毫目、JAXB 和 JBI 等蔬啡。
- 代碼熱替換(HotSwap)、模塊熱部署(Hot Deployment)
開發(fā)者對(duì)程序動(dòng)態(tài)性的追求一直十分火熱镀虐,希望應(yīng)用能像鼠標(biāo)在電腦上熱拔插一樣箱蟆,即插即用,不用重啟電腦刮便。對(duì)應(yīng)軟件開發(fā)上是希望不用重啟應(yīng)用程序即可完成發(fā)布空猜,熱部署對(duì)企業(yè)級(jí)軟件開發(fā)者有很大吸引力。OSGi 是目前 Java 業(yè)界的模塊化標(biāo)準(zhǔn)恨旱,它實(shí)現(xiàn)模塊化熱部署的關(guān)鍵原則是自定義類加載器的實(shí)現(xiàn)辈毯。每一個(gè)模塊都有一個(gè)類加載器,當(dāng)需要更換一個(gè)模塊時(shí)搜贤,連同類加載器一起換掉以實(shí)現(xiàn)熱替換谆沃。OSGi 的類加載器結(jié)構(gòu)不是雙親委派模型那樣的樹形結(jié)構(gòu),而發(fā)展成更為復(fù)雜的網(wǎng)狀結(jié)構(gòu)仪芒。
OSGi 中類加載器的使用時(shí)很值得學(xué)習(xí)的唁影,弄懂了 OSGi的實(shí)現(xiàn),就可以算掌握了類加載器的精髓桌硫。
小結(jié)
本章作者介紹了類加載過程的「加載」夭咬、「驗(yàn)證」、「準(zhǔn)備」铆隘、「解析」和「初始化
」5 個(gè)階段中虛擬機(jī)的動(dòng)作卓舵,還介紹了類加載器的工作原理以及對(duì)虛擬機(jī)的意義。