【貳】JVM-雙親委派機制

一句話概括

  • 雙親委派機制保證了JVM的嚴謹性、安全性
  • 雙親委派機制標準有四層父子關(guān)系classLoader(類加載器)(并非父子繼承關(guān)系秧秉,而是機制定義的上下層關(guān)系
  • 類加載的驗證:每個類由指定path的classLoader加載抹蚀,加載前會先判斷上層父類classLoader是否已加載相同包名+類名的類脊串,若已加載則直接獲取類對象使用窜醉,不會再加載拌汇;若沒加載則繼續(xù)傳遞上層判斷
  • 類加載的執(zhí)行:經(jīng)過驗證判斷均沒加載過該類后缚甩,委派最上級classLoader執(zhí)行加載,若在自己加載范圍內(nèi)無法加載則向下傳遞
  • 打破雙親委派機制:tomcat靶草、SPI蹄胰、OSGi

類加載器classLoader

整個類加載過程任務(wù)非常繁重岳遥,雖然這活兒很累奕翔,但總得有人干。類加載器做的就是上面 5 個步驟的事浩蓉。

如果你在項目代碼里派继,寫一個 java.lang 的包,然后改寫 String 類的一些行為捻艳,編譯后驾窟,發(fā)現(xiàn)并不能生效。JRE 的類當然不能輕易被覆蓋认轨,否則會被別有用心的人利用绅络,這就太危險了。

那類加載器是如何保證這個過程的安全性呢嘁字?其實恩急,它是有著嚴格的等級制度的。

類加載器的種類

  • Bootstrap ClassLoader

這是加載器中的大 Boss纪蜒,任何類的加載行為衷恭,都要經(jīng)它過問。它的作用是加載核心類庫纯续,也就是 rt.jar随珠、resources.jar、charsets.jar 等猬错。當然這些 jar 包的路徑是可以指定的窗看,-Xbootclasspath 參數(shù)可以完成指定操作。

這個加載器是 C++ 編寫的倦炒,隨著 JVM 啟動显沈。

  • Extention ClassLoader

擴展類加載器,主要用于加載 lib/ext 目錄下的 jar 包和 .class 文件析校。同樣的构罗,通過系統(tǒng)變量 java.ext.dirs 可以指定這個目錄。
這個加載器是個 Java 類智玻,繼承自 URLClassLoader遂唧。

  • App ClassLoader

這是我們寫的 Java 類的默認加載器,有時候也叫作 System ClassLoader吊奢。一般用來加載 classpath 下的其他所有 jar 包和 .class 文件盖彭,我們寫的代碼纹烹,會首先嘗試使用這個類加載器進行加載。
這個加載器是個 Java 類召边,繼承自 URLClassLoader铺呵。
它的父類加載器為Extention ClassLoader

  • Custom ClassLoader

自定義加載器,支持一些個性化的擴展功能隧熙。

雙親委派機制

雙親委派機制的意思是除了頂層的啟動類加載器以外片挂,其余的類加載器,在加載之前贞盯,都會委派給它的父加載器進行加載音念。這樣一層層向上傳遞,直到祖先們都無法勝任躏敢,它才會真正的加載闷愤。

打個比方。有一個家族件余,都是一些聽話的孩子讥脐。孫子想要買一塊棒棒糖,最終都要經(jīng)過爺爺過問啼器,如果力所能及旬渠,爺爺就直接幫孫子買了。

但你有沒有想過镀首,“類加載的雙親委派機制坟漱,雙親在哪里?明明都是單親更哄?”

我們還是用一張圖來講解芋齿。可以看到成翩,除了啟動類加載器觅捆,每一個加載器都有一個parent,并沒有所謂的雙親麻敌。但是由于翻譯的問題栅炒,這個叫法已經(jīng)非常普遍了,一定要注意背后的差別术羔。

雙親委派機制

我們可以翻閱 JDK 代碼的 ClassLoader#loadClass 方法赢赊,來看一下具體的加載過程。和我們描述的一樣级历,它首先使用 parent 嘗試進行類加載释移,parent 失敗后才輪到自己。同時寥殖,我們也注意到玩讳,這個方法是可以被覆蓋的涩蜘,也就是雙親委派機制并不一定生效。

ClassLoader#loadClass

這個模型的好處在于 Java 類有了一種優(yōu)先級的層次劃分關(guān)系熏纯。比如 Object 類同诫,這個毫無疑問應(yīng)該交給最上層的加載器進行加載,即使是你覆蓋了它樟澜,最終也是由系統(tǒng)默認的加載器進行加載的误窖。

如果沒有雙親委派模型,就會出現(xiàn)很多個不同的 Object 類往扔,應(yīng)用程序會一片混亂吆录。

詳談雙親委派的好處

這種委托雙親的模式保證了Java核心不會被惡心篡改:

  1. 啟動類加載器可以搶在標準擴展類裝載器之前去裝載類奈偏,而標準擴展類裝載器可以搶在類路徑加載器之前去裝載那個類,類路徑裝載 器又可以搶在自定義類加載器之前去加載它擅羞。所以Java虛擬機先從最可信的Java核心API查找類型嚷堡,這是為了防止不可靠的類扮演被信任的類蝗罗。
    試想一下,網(wǎng)絡(luò)上有個名叫java.lang.Integer的類蝌戒,它是某個黑客為了想混進java.lang包所起的名字串塑,實際上里面含有惡意代碼,但是這種伎倆在雙親模式加載體系結(jié)構(gòu)下是行不通的北苟,因為網(wǎng)絡(luò)類加載器在加載它的時候桩匪,它首先調(diào)用雙親類加載器,這樣一直向上委托友鼻,直到啟動類加載器傻昙,而啟動類加載 器在核心Java API里發(fā)現(xiàn)了這個名字的類,所以它就直接加載Java核心API的java.lang.Integer類彩扔,然后將這個類返回妆档,所以自始自終網(wǎng)絡(luò)上的 java.lang.Integer的類是不會被加載的。

  2. 但是如果這個移動代碼不是去試圖替換一個被信任的類(就是前面說的那種情況)虫碉,而是想在一個被信任的包中插入一個全新的類型贾惦,情況會怎樣呢?
    比如一個名為 java.lang.Virus的類敦捧,經(jīng)過雙親委托模式须板,最終類裝載器試圖從網(wǎng)絡(luò)上下載這個類,因為網(wǎng)絡(luò)類裝載器的雙親們都沒有這個類(當然沒有了兢卵,因為是病毒嘛)习瑰。假設(shè)成功下載了這個類,那你肯定會想济蝉,Virus和lang下的其他類痛在java.lang包下杰刽,暗示這個類是Java API的一部分菠发,那么是不是也擁有修改Java.lang包中數(shù)據(jù)的權(quán)限呢?
    答案當然不是贺嫂,因為要取得訪問和修改java.lang包中的權(quán)限滓鸠,java.lang.Virus和java.lang下其他類必須是屬于同一個運行時包的,什么是運行時包第喳?運行時包是指由同一個類裝載器裝載的糜俗、屬于同一個包的、多個類型的集合曲饱∮颇ǎ考慮一下,java.lang.Virus和java.lang其他類是同一個類裝載器裝載的嗎扩淀?不是的楔敌!java.lang.Virus是由網(wǎng)絡(luò)類裝載器裝載的!

打破雙親委派機制(自定義加載器)

下面我們就來聊一聊可以打破雙親委派機制的一些案例驻谆。為了支持一些自定義加載類多功能的需求卵凑,Java 設(shè)計者其實已經(jīng)作出了一些妥協(xié)。

案例一:tomcat

tomcat 通過 war 包進行應(yīng)用的發(fā)布胜臊,它其實是違反了雙親委派機制原則的勺卢。簡單看一下 tomcat 類加載器的層次結(jié)構(gòu)。

打破雙親委派-tomcat

對于一些需要加載的非基礎(chǔ)類象对,會由一個叫作 WebAppClassLoader 的類加載器優(yōu)先加載黑忱。等它加載不到的時候,再交給上層的 ClassLoader 進行加載勒魔。這個加載器用來隔絕不同應(yīng)用的 .class 文件甫煞,比如你的兩個應(yīng)用,可能會依賴同一個第三方的不同版本沥邻,它們是相互沒有影響的危虱。

如何在同一個 JVM 里,運行著不兼容的兩個版本唐全,當然是需要自定義加載器才能完成的事埃跷。

那么 tomcat 是怎么打破雙親委派機制的呢?可以看圖中的 WebAppClassLoader邮利,它加載自己目錄下的 .class 文件弥雹,并不會傳遞給父類的加載器。但是延届,它卻可以使用 SharedClassLoader 所加載的類剪勿,實現(xiàn)了共享和分離的功能。

但是你自己寫一個 ArrayList方庭,放在應(yīng)用目錄里厕吉,tomcat 依然不會加載酱固。它只是自定義的加載器順序不同,但對于頂層來說头朱,還是一樣的运悲。(也就是仍會自上而下檢查是否有相同類已經(jīng)加載)

案例二:SPI

Java 中有一個 SPI 機制,全稱是 Service Provider Interface项钮,是 Java 提供的一套用來被第三方實現(xiàn)或者擴展的 API班眯,它可以用來啟用框架擴展和替換組件。

這個說法可能比較晦澀烁巫,但是拿我們常用的數(shù)據(jù)庫驅(qū)動加載來說署隘,就比較好理解了。在使用 JDBC 寫程序之前亚隙,通常會調(diào)用下面這行代碼磁餐,用于加載所需要的驅(qū)動類。

Class.forName("com.mysql.jdbc.Driver")

這只是一種初始化模式恃鞋,通過 static 代碼塊顯式地聲明了驅(qū)動對象崖媚,然后把這些信息,保存到底層的一個 List 中恤浪。但你會發(fā)現(xiàn),即使刪除了 Class.forName 這一行代碼肴楷,也能加載到正確的驅(qū)動類水由。
簡而言之,通過在 META-INF/services 目錄下赛蔫,創(chuàng)建一個以接口全限定名為命名的文件(內(nèi)容為實現(xiàn)類的全限定名)砂客,即可自動加載這一種實現(xiàn),這就是 SPI呵恢。
SPI 實際上是“基于接口的編程+策略模式+配置文件”組合實現(xiàn)的動態(tài)加載機制鞠值,主要使用 java.util.ServiceLoader 類進行動態(tài)裝載。

但這個ServiceLoader是屬于rt.jar的渗钉,它的類加載器是 Bootstrap ClassLoader彤恶,也就是最上層的那個。而具體的數(shù)據(jù)庫驅(qū)動鳄橘,卻屬于業(yè)務(wù)代碼声离,這個啟動類加載器是無法加載的。

即ServiceLoader加載時是由Bootstrap ClassLoader加載瘫怜,但加載ServiceLoader必須加載具體的類方法實現(xiàn)术徊,而類方法實現(xiàn)在應(yīng)用代碼,因由App ClassLoader加載鲸湃,故出現(xiàn)了反向雙親委派的情況赠涮。

在Java應(yīng)用中存在著很多服務(wù)提供者接口(Service Provider Interface子寓,SPI),這些接口允許第三方為它們提供實現(xiàn)笋除,如常見的 SPI 有 JDBC别瞭、JNDI等,這些 SPI 的接口屬于 Java 核心庫株憾,一般存在rt.jar包中蝙寨,由Bootstrap類加載器加載。而Bootstrap類加載器無法直接加載SPI的實現(xiàn)類嗤瞎,同時由于雙親委派模式的存在墙歪,Bootstrap類加載器也無法反向委托AppClassLoader加載器SPI的實現(xiàn)類。在這種情況下贝奇,我們就需要一種特殊的類加載器來加載第三方的類庫虹菲,而 線程上下文類加載器(雙親委派模型的破壞者)就是很好的選擇。

打破雙親委派-SPI(線程上下文類加載器)

從圖可知rt.jar核心包是有Bootstrap類加載器加載的掉瞳,其內(nèi)包含SPI核心接口類毕源,由于SPI中的類經(jīng)常需要調(diào)用外部實現(xiàn)類的方法,而jdbc.jar包含外部實現(xiàn)類(jdbc.jar存在于classpath路徑)無法通過Bootstrap類加載器加載陕习,因此只能委派線程上下文類加載器把jdbc.jar中的實現(xiàn)類加載到內(nèi)存以便SPI相關(guān)類使用霎褐。顯然這種線程上下文類加載器的加載方式破壞了“雙親委派模型”,它在執(zhí)行過程中拋棄雙親委派加載鏈模式该镣,使程序可以逆向使用類加載器冻璃,當然這也使得Java類加載器變得更加靈活。

我們可以一步步跟蹤代碼损合,來看一下這個過程省艳。

//part1:DriverManager::loadInitialDrivers
  //jdk1.8 之后,變成了lazy的ensureDriversInitialized
  ...
  ServiceLoader <Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  Iterator<Driver> driversIterator = loadedDrivers.iterator();
  ...

  //part2:ServiceLoader::load
  public static <T> ServiceLoader<T> load(Class<T> service) {
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return ServiceLoader.load(service, cl);
  }

通過代碼你可以發(fā)現(xiàn) Java 玩了個魔術(shù)嫁审,它把當前的類加載器跋炕,設(shè)置成了線程上下文類加載器。那么律适,對于一個剛剛啟動的應(yīng)用程序來說辐烂,它當前的線程上下文加載器是誰呢?也就是說擦耀,啟動 main 方法的那個加載器棉圈,到底是哪一個?

所以我們繼續(xù)跟蹤代碼眷蜓。找到 Launcher 類分瘾,就是 jre 中用于啟動入口函數(shù) main 的類。我們在 Launcher 中找到以下代碼。

public Launcher() {
 Launcher.ExtClassLoader var1;
 try {
     var1 = Launcher.ExtClassLoader.getExtClassLoader();
 } catch (IOException var10) {
     throw new InternalError("Could not create extension class loader", var10);
 }
 
 try {
     this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
 } catch (IOException var9) {
     throw new InternalError("Could not create application class loader", var9);
 }
 Thread.currentThread().setContextClassLoader(this.loader);
 ...
 }

到此為止德召,事情就比較明朗了白魂,當前線程上下文類加載器,是應(yīng)用程序類加載器上岗。使用它來加載第三方驅(qū)動福荸,是沒有什么問題的。

這樣就可以更好的看清一個打破規(guī)則的案例肴掷。

案例三:OSGi

OSGi 曾經(jīng)非常流行敬锐,Eclipse 就使用 OSGi 作為插件系統(tǒng)的基礎(chǔ)。OSGi 是服務(wù)平臺的規(guī)范呆瞻,旨在用于需要長運行時間台夺、動態(tài)更新和對運行環(huán)境破壞最小的系統(tǒng)。

OSGi 規(guī)范定義了很多關(guān)于包生命周期痴脾,以及基礎(chǔ)架構(gòu)和綁定包的交互方式颤介。這些規(guī)則,通過使用特殊 Java 類加載器來強制執(zhí)行赞赖,比較霸道滚朵。

比如,在一般 Java 應(yīng)用程序中前域,classpath 中的所有類都對所有其他類可見辕近,這是毋庸置疑的。但是话侄,OSGi 類加載器基于 OSGi 規(guī)范和每個綁定包的 manifest.mf 文件中指定的選項亏推,來限制這些類的交互,這就讓編程風(fēng)格變得非常的怪異年堆。但我們不難想象,這種與直覺相違背的加載方式盏浇,肯定是由專用的類加載器來實現(xiàn)的变丧。

隨著 jigsaw 的發(fā)展(旨在為 Java SE 平臺設(shè)計、實現(xiàn)一個標準的模塊系統(tǒng))绢掰,我個人認為痒蓬,現(xiàn)在的 OSGi,意義已經(jīng)不是很大了滴劲。OSGi 是一個龐大的話題攻晒,你只需要知道,有這么一個復(fù)雜的東西班挖,實現(xiàn)了模塊化鲁捏,每個模塊可以獨立安裝、啟動萧芙、停止给梅、卸載假丧,就可以了。

不過动羽,如果你有機會接觸相關(guān)方面的工作包帚,也許會不由的發(fā)出感嘆:原來 Java 的類加載器,可以玩出這么多花樣运吓。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末渴邦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拘哨,更是在濱河造成了極大的恐慌谋梭,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宅静,死亡現(xiàn)場離奇詭異章蚣,居然都是意外死亡,警方通過查閱死者的電腦和手機姨夹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門纤垂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人磷账,你說我怎么就攤上這事峭沦。” “怎么了逃糟?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵吼鱼,是天一觀的道長。 經(jīng)常有香客問我绰咽,道長菇肃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任取募,我火速辦了婚禮琐谤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘玩敏。我一直安慰自己斗忌,他們只是感情好,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布旺聚。 她就那樣靜靜地躺著织阳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪砰粹。 梳的紋絲不亂的頭發(fā)上唧躲,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機與錄音,去河邊找鬼惊窖。 笑死刽宪,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的界酒。 我是一名探鬼主播圣拄,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼毁欣!你這毒婦竟也來了庇谆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤凭疮,失蹤者是張志新(化名)和其女友劉穎饭耳,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體执解,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡寞肖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了衰腌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片新蟆。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖右蕊,靈堂內(nèi)的尸體忽然破棺而出琼稻,到底是詐尸還是另有隱情饶囚,我是刑警寧澤嘀掸,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布卿拴,位于F島的核電站,受9級特大地震影響缘挽,放射性物質(zhì)發(fā)生泄漏苏研。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一过咬、第九天 我趴在偏房一處隱蔽的房頂上張望泵三。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抚垄。三九已至桐经,卻和暖如春毁兆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背阴挣。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工气堕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人屯吊。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓送巡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親盒卸。 傳聞我的和親對象是個殘疾皇子骗爆,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容