阿里高頻面試題,熱部署了解嗎?
本文轉(zhuǎn)載自微信公眾號(hào)「稀飯下雪」,作者帥氣的小飯飯。轉(zhuǎn)載本文請(qǐng)聯(lián)系稀飯下雪公眾號(hào)。
熟悉雙親委派模型嗎?
熟悉,這題我會(huì)。
機(jī)會(huì)來(lái)了,我八股文大神可是專門修煉過(guò)這個(gè)的。
我們?yōu)樯恫荒茉贘ava工程中定義同名的String的Java文件?
(⊙o⊙)…
這.....
多線程加載類的時(shí)候?yàn)槭裁礇](méi)有線程問(wèn)題?
臥槽 ....
首先,在講解這道題之前,先來(lái)劃重點(diǎn)
類加載時(shí)機(jī)與過(guò)程
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個(gè)階段。其中準(zhǔn)備、驗(yàn)證、解析3個(gè)部分統(tǒng)稱為連接(Linking)
加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這5個(gè)階段的順序是確定的,類的加載過(guò)程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定(也稱為動(dòng)態(tài)綁定或晚期綁定)
加載
在加載階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機(jī)需要完成以下3件事情:
- 通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流(并沒(méi)有指明要從一個(gè)Class文件中獲取,可以從其他渠道,譬如:網(wǎng)絡(luò)、動(dòng)態(tài)生成、數(shù)據(jù)庫(kù)等);
- 將這個(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)入口;
驗(yàn)證
驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。驗(yàn)證階段大致會(huì)完成4個(gè)階段的檢驗(yàn)動(dòng)作:
- 文件格式驗(yàn)證;
- 元數(shù)據(jù)驗(yàn)證;
- 字節(jié)碼驗(yàn)證;
- 符號(hào)引用驗(yàn)證;
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在堆中。其次,這里所說(shuō)的初始值「通常情況」下是數(shù)據(jù)類型的零值,
例如:public static int a = 1
這個(gè)時(shí)候變量a在準(zhǔn)備階段過(guò)后的初始值為0,而不是1,因?yàn)檫@個(gè)時(shí)候尚未開始執(zhí)行任何java方法,而把a(bǔ)賦值為1的指令是程序被編譯后,存放于類構(gòu)造器()方法中,所以把a(bǔ)賦值為1的動(dòng)作將在初始化階段才執(zhí)行。
而當(dāng):public static final int a = 2
這種情況又不同了,當(dāng)類字段的屬性是ConstantValue時(shí),會(huì)在準(zhǔn)備階段初始化為指定的值,所以標(biāo)志位final之后,a的值再準(zhǔn)備階段便被初始化為2,而不再是1 了。
劃重點(diǎn):這里其實(shí)也好理解,final標(biāo)志的常量意味著是不變的,那么在準(zhǔn)備階段直接賦值了就可以了,沒(méi)必要多此一舉,先賦初始值,再在初始化的時(shí)候額外再做一次。
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程。解析動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行。
初始化
在介紹初始化時(shí),要先介紹兩個(gè)方法:
在編譯生成class文件時(shí),會(huì)自動(dòng)產(chǎn)生兩個(gè)方法,一個(gè)是類的初始化方法, 另一個(gè)是實(shí)例的初始化方法
類初始化階段是類加載過(guò)程的最后一步,到了初始化階段,才真正開始執(zhí)行類中定義的java程序代碼。在準(zhǔn)備階段、變量已經(jīng)被賦值過(guò)一次系統(tǒng)要求的初始值,而在初始化階段,則根據(jù)開發(fā)人員通過(guò)程序去初始化類變量和其他資源,或者說(shuō):初始化階段是執(zhí)行類構(gòu)造器
「總結(jié)一大波美女:其實(shí)也這個(gè)過(guò)程不用記,靠理解最好了,你想想看哦,你要把一個(gè)類加載到虛擬機(jī),是不是要有一個(gè)加載的過(guò)程,這個(gè)過(guò)程就是將你的class對(duì)應(yīng)的二進(jìn)制字節(jié)流加載到虛擬機(jī)中,加載完虛擬機(jī)是不是要校驗(yàn)下,規(guī)則的你給的東西有沒(méi)有毒?驗(yàn)證完了,是不是要幫我們把一些類變量設(shè)置下初始值?設(shè)置完了還有一些對(duì)象標(biāo)志啊什么的,是不是也要幫忙解析成直接引用,最后是不是還要走下我們類的初始化方法?!?/p>
雙親委派模型
所謂雙親委派是指每次收到類加載請(qǐng)求時(shí),先將請(qǐng)求委派給父類加載器完成(所有加載請(qǐng)求最終會(huì)委派到頂層的Bootstrap ClassLoader加載器中),如果父類加載器無(wú)法完成這個(gè)加載(該加載器的「搜索范圍」中沒(méi)有找到對(duì)應(yīng)的類),子類嘗試自己加載, 如果都沒(méi)加載到,則會(huì)拋出 ClassNotFoundException 異常, 看到這里其實(shí)就解釋了文章開頭提出的第一個(gè)問(wèn)題,父加載器已經(jīng)加載了JDK 中的 String.class 文件,所以我們不能定義同名的 String java 文件。
這么做的好處是什么?
因?yàn)檫@樣可以避免重復(fù)加載,當(dāng)父親已經(jīng)加載了該類的時(shí)候,就沒(méi)有必要 ClassLoader 再加載一次??紤]到安全因素,我們?cè)囅胍幌?,如果不使用這種委托模式,那我們就可以隨時(shí)使用自定義的String來(lái)動(dòng)態(tài)替代java核心api中定義的類型,這樣會(huì)存在非常大的安全隱患,而雙親委托的方式,就可以避免這種情況,因?yàn)镾tring 已經(jīng)在啟動(dòng)時(shí)就被引導(dǎo)類加載器(Bootstrcp ClassLoader)加載,所以用戶自定義的ClassLoader永遠(yuǎn)也無(wú)法加載一個(gè)自己寫的String,除非你改變 JDK 中 ClassLoader 搜索類的默認(rèn)算法。
我們發(fā)現(xiàn)除了啟動(dòng)類加載器(BootStrap ClassLoader),每個(gè)類都有其"父類"加載器,這種組合關(guān)系是通過(guò)組合模式來(lái)決定的,而不是繼承關(guān)系。
三種類加載器分別職責(zé)是啥?
- 「Bootstrap ClassLoader」:這個(gè)加載器不是一個(gè)Java類,而是由底層的c++實(shí)現(xiàn),負(fù)責(zé)在虛擬機(jī)啟動(dòng)時(shí)加載Jdk核心類庫(kù)(如:rt.jar、resources.jar、charsets.jar等)以及加載后兩個(gè)類加載器。這個(gè)ClassLoader完全是JVM自己控制的,需要加載哪個(gè)類,怎么加載都是由JVM自己控制,別人也訪問(wèn)不到這個(gè)類
- 「Extension ClassLoader」:是一個(gè)普通的Java類,繼承自ClassLoader類,負(fù)責(zé)加載{JAVA_HOME}/jre/lib/ext/目錄下的所有jar包。
- 「App ClassLoader」:是Extension ClassLoader的子對(duì)象,負(fù)責(zé)加載應(yīng)用程序classpath目錄下的所有jar和class文件。
通常用這兩種方式來(lái)動(dòng)態(tài)加載一個(gè) java 類,「Class.forName()」 與 「ClassLoader.loadClass()」 ,但是兩個(gè)方法之間也是有一些細(xì)微的差別,具體看上一篇文章:Object詳談
雙親委派模型源碼解析
來(lái)看看核心loadClass的方法
可以看到方法有同步塊(synchronized), 這樣就算多線程情況也不會(huì)出現(xiàn)重復(fù)加載的情況。
JAVA熱部署實(shí)現(xiàn)
什么是熱部署(hotswap)?
Java 類是通過(guò) Java 虛擬機(jī)加載的,某個(gè)類的 class 文件在被 classloader 加載后,會(huì)生成對(duì)應(yīng)的 Class 對(duì)象,之后就可以創(chuàng)建該類的實(shí)例,而熱部署是在不重啟 Java 虛擬機(jī)的這一前提下,自動(dòng)偵測(cè)到 class 文件的變化,更新運(yùn)行時(shí) class 的行為。
而實(shí)際上,對(duì)于同一個(gè)全限定名的java類,只能被加載一次,而且無(wú)法被卸載。
那么竟然類無(wú)法被卸載,那么能不能把她媽給換了?也就是類加載器,答案是可以的,我們可以自定義類的加載器,并重寫ClassLoader的findClass方法,然后熱部署步驟就是:
- 銷毀自定義ClassLoader(被該加載器加載的class也會(huì)自動(dòng)卸載);
- 更新class
- 使用新的ClassLoader去加載class
最終落地到實(shí)現(xiàn),其實(shí)就是將ClassLoader給覆蓋掉,怎么覆蓋,重新new一個(gè)同樣的ClassLoader就可以了。
「總結(jié)一大波:我們雖然不能對(duì)被加載的class動(dòng)手動(dòng)腳,但是卻可以對(duì)ClassLoader動(dòng)手腳,搞掉它,然后使用新的ClassLoader加載class就可以了?!?/p>
那么什么時(shí)候回收掉老的類呢?畢竟替換了,就存在新舊問(wèn)題了,也就存在內(nèi)存泄露問(wèn)題了。
通常情況下,在JSP,OSGI及其他一些支持熱替換的庫(kù),都是需要進(jìn)行類的卸載回收的,否則類在替換后,老的類就沒(méi)用了但是還在內(nèi)存中,就會(huì)造成內(nèi)存泄漏。
類的卸載需要滿足以下三個(gè)條件:
- 該類所有的實(shí)例都已經(jīng)被GC,也就是JVM中不存在該Class的任何實(shí)例。
- 加載該類的ClassLoader已經(jīng)被GC。
- 該類的java.lang.Class 對(duì)象沒(méi)有在任何地方被引用,如不能在任何地方通過(guò)反射訪問(wèn)該類的方法。
所以在自定義類加載器時(shí),就要注意這一點(diǎn),如果你是希望其使用完成后就被卸載,那么就需要特別留意類加載器及類的作用域了。
原文鏈接:https://mp.weixin.qq.com/s/JT4Vh_tXzNAQdLSAosy2cQ