Java歷史上有三次破壞雙親委派模型,是哪三次?
本文轉(zhuǎn)載自微信公眾號(hào)「yes的練級(jí)攻略」,作者是Yes呀。轉(zhuǎn)載本文請(qǐng)聯(lián)系yes的練級(jí)攻略公眾號(hào)。
你好,我是yes。
這個(gè)面試題來(lái)自一位群友的面試題分享,就是我組建的那個(gè)面試交流群。
其實(shí)不止三次,有四次。
今天我們就來(lái)盤(pán)一盤(pán)這個(gè)面試題,不過(guò)在說(shuō)雙親委派模型之前,我們得先簡(jiǎn)單了解下類(lèi)加載。
類(lèi)加載
我們平常寫(xiě)的代碼是保存在一個(gè) .java文件里面,經(jīng)過(guò)編譯會(huì)生成.class文件,這個(gè)文件存儲(chǔ)的就是字節(jié)碼,如果要用上我們的代碼,那就必須把它加載到 JVM 中。
當(dāng)然,加載到 JVM 生成 class 對(duì)象的來(lái)源不一定得是.class文件,也可以來(lái)自網(wǎng)絡(luò)等等,反正只要是符合 JVM 規(guī)范的都行。
而類(lèi)加載的步驟主要分為:加載、鏈接、初始化。
加載
其實(shí)就是找到字節(jié)流,然后將其加載到 JVM 中,生成類(lèi)對(duì)象。這個(gè)階段就是類(lèi)加載器派上用場(chǎng)的階段,等下我們?cè)偌?xì)說(shuō)。
鏈接
這個(gè)階段是要讓生成的類(lèi)對(duì)象融入到 JVM 中,分別要經(jīng)歷以下三個(gè)步驟:
驗(yàn)證就是檢驗(yàn)一下加載的類(lèi)是否滿足 JVM 的約束條件,也就是判斷是否合規(guī)。
準(zhǔn)備就是為加載類(lèi)的靜態(tài)變量申請(qǐng)內(nèi)存空間,并賦予初始值,例如是 int 類(lèi)型那初始值就是 0。
解析就是將符號(hào)引用解析成為實(shí)際引用,講人話就是:例如 Yes 類(lèi)里面引用了一個(gè) XX 類(lèi),那一開(kāi)始 Yes 類(lèi)肯定不知道 XX 類(lèi)在內(nèi)存里面的地址,所以就先搞個(gè)符號(hào)引用替代一下,假裝知道,等類(lèi)加載解析的時(shí)候再找到 XX 類(lèi)真正地址,做一個(gè)實(shí)際引用。
這就是解析要做的事情。還有一點(diǎn),雖說(shuō)把解析放到鏈接階段里面,但是 JVM 規(guī)范并沒(méi)有要求在鏈接過(guò)程中完成解析。
初始化
這個(gè)階段就是為常量字段賦值,然后執(zhí)行靜態(tài)代碼塊,將一堆要執(zhí)行的靜態(tài)代碼塊方法包裝成 clinit 方法執(zhí)行,這個(gè)方法會(huì)加鎖,由 JVM 來(lái)保證 clinit 方法只會(huì)被執(zhí)行一次。
所以可以用一個(gè)內(nèi)部靜態(tài)類(lèi)來(lái)實(shí)現(xiàn)延遲初始化的單例設(shè)計(jì)模式,同時(shí)保證了線程安全。
這個(gè)階段完畢之后,類(lèi)加載過(guò)程就 ok 了,可以投入使用啦,再來(lái)畫(huà)個(gè)圖匯總一下:
雙親委派模型
加載階段,需要用到類(lèi)加載器來(lái)將 class 文件里面的內(nèi)容搞到 JVM 中生成類(lèi)對(duì)象。
那什么是雙親委派模型?
雙親委派模型用一句話講就是子類(lèi)加載器先讓父類(lèi)加載器去查找該類(lèi)來(lái)加載,父類(lèi)又繼續(xù)請(qǐng)求它的父類(lèi)直到最頂層,在父類(lèi)加載器沒(méi)有找到所請(qǐng)求的類(lèi)的情況下,子類(lèi)加載器才會(huì)嘗試去加載,這樣一層一層上去又下來(lái)。
每個(gè)類(lèi)加載器都有固定的查找類(lèi)的路徑,在 JDK8 的時(shí)候一共有三種類(lèi)加載器。
- 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader),它是屬于虛擬機(jī)自身的一部分,用 C++ 實(shí)現(xiàn)的,主要負(fù)責(zé)加載
\lib目錄中或被 -Xbootclasspath 指定的路徑中的并且文件名是被虛擬機(jī)識(shí)別的文件。它是所有類(lèi)加載器的爸爸。 - 擴(kuò)展類(lèi)加載器(Extension ClassLoader),它是 Java 實(shí)現(xiàn)的,獨(dú)立于虛擬機(jī),主要負(fù)責(zé)加載
\lib\ext目錄中或被 java.ext.dirs 系統(tǒng)變量所指定的路徑的類(lèi)庫(kù)。 - 應(yīng)用程序類(lèi)加載器(Application ClassLoader),它是Java實(shí)現(xiàn)的,獨(dú)立于虛擬機(jī)。主要負(fù)責(zé)加載用戶類(lèi)路徑(classPath)上的類(lèi)庫(kù),如果我們沒(méi)有實(shí)現(xiàn)自定義的類(lèi)加載器那這玩意就是我們程序中的默認(rèn)加載器。
為什么要提出雙親委派模型?
其實(shí)就是為了讓基礎(chǔ)類(lèi)得以正確地統(tǒng)一地加載。
從上面的圖可以看出,如果你也定義了一個(gè) java.lang.Object類(lèi),通過(guò)雙親委派模式是會(huì)把這個(gè)請(qǐng)求委托給啟動(dòng)類(lèi)加載器,它掃描
至此我們已經(jīng)清楚了什么是雙親委派,和為什么要雙親委派。接下來(lái)我們來(lái)看看三次破壞。
第一次破壞
在 jdk 1.2 之前,那時(shí)候還沒(méi)有雙親委派模型,不過(guò)已經(jīng)有了 ClassLoader 這個(gè)抽象類(lèi),所以已經(jīng)有人繼承這個(gè)抽象類(lèi),重寫(xiě) loadClass 方法來(lái)實(shí)現(xiàn)用戶自定義類(lèi)加載器。
而在 1.2 的時(shí)候要引入雙親委派模型,為了向前兼容, loadClass 這個(gè)方法還得保留著使之得以重寫(xiě),新搞了個(gè) findClass 方法讓用戶去重寫(xiě),并呼吁大家不要重寫(xiě) loadClass 只要重寫(xiě) findClass。
這就是第一次對(duì)雙親委派模型的破壞,因?yàn)殡p親委派的邏輯在 loadClass 上,但是又允許重寫(xiě) loadClass,重寫(xiě)了之后就可以破壞委派邏輯了。
第二次破壞
第二次破壞指的是 JNDI、JDBC 之類(lèi)的情況。
首先得知道什么是 SPI(Service Provider Interface),它是面向拓展的,也就是說(shuō)我定義了個(gè)規(guī)矩,就是 SPI ,具體如何實(shí)現(xiàn)由擴(kuò)展者實(shí)現(xiàn)。
像我們比較熟的 JDBC 就是如此。
MySQL 有 MySQL 的 JDBC 實(shí)現(xiàn),Oracle 有 Oracle 的 JDBC 實(shí)現(xiàn),我 Java 不管你內(nèi)部如何實(shí)現(xiàn)的,反正你們這些數(shù)據(jù)庫(kù)廠商都得統(tǒng)一按我這個(gè)來(lái),這樣我們 Java 開(kāi)發(fā)者才能容易的調(diào)用數(shù)據(jù)庫(kù)操作,所以在 Java 核心包里面定義了這個(gè) SPI。
而核心包里面的類(lèi)都是由啟動(dòng)類(lèi)加載器去加載的,但它的手只能摸到
而 JDBC 的實(shí)現(xiàn)類(lèi)在我們用戶定義的 classpath 中,只能由應(yīng)用類(lèi)加載器去加載,所以啟動(dòng)類(lèi)加載器只能委托子類(lèi)來(lái)加載數(shù)據(jù)庫(kù)廠商們提供的具體實(shí)現(xiàn),這就違反了自下而上的委托機(jī)制。
具體解決辦法是搞了個(gè)線程上下文類(lèi)加載器,通過(guò)setContextClassLoader()默認(rèn)情況就是應(yīng)用程序類(lèi)加載器,然后利用Thread.current.currentThread().getContextClassLoader()獲得類(lèi)加載器來(lái)加載。
這就是第二次破壞雙親委派模型。
第三次破壞
這次破壞是為了滿足熱部署的需求,不停機(jī)更新這對(duì)企業(yè)來(lái)說(shuō)至關(guān)重要,畢竟停機(jī)是大事。
OSGI 就是利用自定義的類(lèi)加載器機(jī)制來(lái)完成模塊化熱部署,而它實(shí)現(xiàn)的類(lèi)加載機(jī)制就沒(méi)有完全遵循自下而上的委托,有很多平級(jí)之間的類(lèi)加載器查找,具體就不展開(kāi)了,有興趣可以自行研究一下。
這就是第三次破壞。
第四次破壞
在 JDK9 引入模塊系統(tǒng)之后,類(lèi)加載器的實(shí)現(xiàn)其實(shí)做了一波更新。
像擴(kuò)展類(lèi)加載器被重命名為平臺(tái)類(lèi)加載器,核心類(lèi)加載歸屬了做了一些劃分,平臺(tái)類(lèi)加載器承擔(dān)了更多的類(lèi)加載,上面提到的 -Xbootclasspath、java.ext.dirs 也都無(wú)效了,rt.jar 之類(lèi)的也被移除,被整理存儲(chǔ)在 jimage 文件中,通過(guò)新的 JRT 文件系統(tǒng)訪問(wèn)。
當(dāng)收到類(lèi)加載請(qǐng)求,會(huì)先判斷該類(lèi)在具名模塊中是否有定義,如果有定義就自己加載了,沒(méi)的話再委派給父類(lèi)。
關(guān)于 JDK9 相關(guān)的知識(shí)點(diǎn)就不展開(kāi)了,有興趣的自行查閱。
所以這就是第四次破壞。
其他注意點(diǎn)
首先,雖說(shuō)是子類(lèi)父類(lèi),但是加載器之間的關(guān)系不是繼承,而是組合。
看下代碼就很清晰了,具體的邏輯如下:
在 JVM 中,類(lèi)的唯一性是由類(lèi)加載器實(shí)例和類(lèi)的全限定名一同確定的,也就是說(shuō)即使是同一個(gè)類(lèi)文件加載的類(lèi),用不同的類(lèi)加載器實(shí)例加載,在 JVM 看來(lái)這也是兩個(gè)類(lèi)。
所以說(shuō)類(lèi)加載器還有命名空間的作用,我記得這個(gè)知識(shí)點(diǎn)也是一個(gè)面試題喲~