一句話概括
- 雙親委派機制保證了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 失敗后才輪到自己。同時寥殖,我們也注意到玩讳,這個方法是可以被覆蓋的涩蜘,也就是雙親委派機制并不一定生效。
這個模型的好處在于 Java 類有了一種優(yōu)先級的層次劃分關(guān)系熏纯。比如 Object 類同诫,這個毫無疑問應(yīng)該交給最上層的加載器進行加載,即使是你覆蓋了它樟澜,最終也是由系統(tǒng)默認的加載器進行加載的误窖。
如果沒有雙親委派模型,就會出現(xiàn)很多個不同的 Object 類往扔,應(yīng)用程序會一片混亂吆录。
詳談雙親委派的好處
這種委托雙親的模式保證了Java核心不會被惡心篡改:
啟動類加載器可以搶在標準擴展類裝載器之前去裝載類奈偏,而標準擴展類裝載器可以搶在類路徑加載器之前去裝載那個類,類路徑裝載 器又可以搶在自定義類加載器之前去加載它擅羞。所以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的類是不會被加載的。但是如果這個移動代碼不是去試圖替換一個被信任的類(就是前面說的那種情況)虫碉,而是想在一個被信任的包中插入一個全新的類型贾惦,情況會怎樣呢?
比如一個名為 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)。
對于一些需要加載的非基礎(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)類。在這種情況下贝奇,我們就需要一種特殊的類加載器來加載第三方的類庫虹菲,而 線程上下文類加載器(雙親委派模型的破壞者)就是很好的選擇。
從圖可知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 的類加載器,可以玩出這么多花樣运吓。