秒懂雙親委派機(jī)制
前言
最近知識(shí)星球中,有位小伙伴問了我一個(gè)問題:JDBC為什么會(huì)破壞雙親委派機(jī)制?
這個(gè)問題挺有代表性的。
雙親委派機(jī)制是Java中非常重要的類加載機(jī)制,它保證了類加載的完整性和安全性,避免了類的重復(fù)加載。
這篇文章就跟大家一起聊聊,Java中類加載的雙親委派機(jī)制到底是怎么回事,有哪些破壞雙親委派機(jī)制的案例,為什么要破壞雙親委派機(jī)制,希望對(duì)你會(huì)有所幫助。
1 為什么要雙親委派機(jī)制?
我們的Java在運(yùn)行之前,首先需要把Java代碼轉(zhuǎn)換成字節(jié)碼,即class文件。
然后JVM需要把字節(jié)碼通過一定的方式加載到內(nèi)存中的運(yùn)行時(shí)數(shù)據(jù)區(qū)。
這種方式就是類加載器(ClassLoader)。
再通過加載、驗(yàn)證、準(zhǔn)備、解析、初始化這幾個(gè)步驟完成類加載過程,然后再由jvm執(zhí)行引擎的解釋器和JIT即時(shí)編譯器去將字節(jié)碼指令轉(zhuǎn)換為本地機(jī)器指令進(jìn)行執(zhí)行。
我們?cè)谑褂妙惣虞d器加載類的時(shí)候,會(huì)面臨下面幾個(gè)問題:
- 如何保證類不會(huì)被重復(fù)加載?類重復(fù)加載會(huì)出現(xiàn)很多問題。
- 類加載器是否允許用戶自定義?
- 如果允許用戶自定義,如何保證類文件的安全性?
- 如何保證加載的類的完整性?
為了解決上面的這一系列的問題,我們必須要引入某一套機(jī)制,這套機(jī)制就是:雙親委派機(jī)制。
2 什么是雙親委派機(jī)制?
接下來,我們看看什么是雙親委派機(jī)制。
雙親委派機(jī)制的基本思想是:當(dāng)一個(gè)類加載器試圖加載某個(gè)類時(shí),它會(huì)先委托給其父類加載器,如果父類加載器無法加載,再由當(dāng)前類加載器自己進(jìn)行加載。
這種層層委派的方式有助于保障類的唯一性,避免類的重復(fù)加載,并提高系統(tǒng)的安全性和穩(wěn)定性。
在Java中默認(rèn)的類加載器有3層:
- 啟動(dòng)類加載器(Bootstrap Class Loader):負(fù)責(zé)加載 %JAVA_HOME%/jre/lib 目錄下的核心Java類庫,比如:rt.jar、charsets.jar等。它是最頂層的類加載器,通常由C++編寫。
- 擴(kuò)展類加載器(Extension Class Loader):負(fù)責(zé)加載Java的擴(kuò)展庫,一般位于<JAVA_HOME>/lib/ext目錄下。
- 應(yīng)用程序類加載器(Application Class Loader):也稱為系統(tǒng)類加載器,負(fù)責(zé)加載用戶類路徑(ClassPath)下的應(yīng)用程序類。
用一張圖梳理一下,雙親委派機(jī)制中的3種類加載器的層次關(guān)系:
圖片
但這樣不夠靈活,用戶沒法控制,加載自己想要的一些類。
于是,Java中引入了自定義類加載器。
創(chuàng)建一個(gè)新的類并繼承ClassLoader類,然后重寫findClass方法。
該方法主要是實(shí)現(xiàn)從那個(gè)路徑讀取 ar包或者.class文件,將讀取到的文件用字節(jié)數(shù)組來存儲(chǔ),然后可以使用父類的defineClass來轉(zhuǎn)換成字節(jié)碼。
如果想破壞雙親委派的話,就重寫loadClass方法,否則不用重寫。
類加載器的層次關(guān)系改成:
圖片
雙親委派機(jī)制流程圖如下:
圖片
具體流程大概是這樣的:
- 需要加載某個(gè)類時(shí),先檢查自定義類加載器是否加載過,如果已經(jīng)加載過,則直接返回。
- 如果自定義類加載器沒有加載過,則檢查應(yīng)用程序類加載器是否加載過,如果已經(jīng)加載過,則直接返回。
- 如果應(yīng)用程序類加載器沒有加載過,則檢查擴(kuò)展類加載器是否加載過,如果已經(jīng)加載過,則直接返回。
- 如果擴(kuò)展類加載器沒有加載過,則檢查啟動(dòng)類加載器是否加載過,如果已經(jīng)加載過,則直接返回。
- 如果啟動(dòng)類加載器沒有加載過,則判斷當(dāng)前類加載器能否加載這個(gè)類,如果能加載,則加載該類,然后返回。
- 如果啟動(dòng)類加載器不能加載該類,則交給擴(kuò)展類加載器。擴(kuò)展類加載器判斷能否加載這個(gè)類,如果能加載,則加載該類,然后返回。
- 如果擴(kuò)展類加載器不能加載該類,則交給應(yīng)用程序類加載器。應(yīng)用程序類加載器判斷能否加載這個(gè)類,如果能加載,則加載該類,然后返回。
- 如果應(yīng)用程序類加載器不能加載該類,則交給自定義類加載器。自定義類加載器判斷能否加載這個(gè)類,如果能加載,則加載該類,然后返回。
- 如果自定義類加載器,也無法加載這個(gè)類,則直接拋ClassNotFoundException異常。
這樣做的好處是:
- 保證類不會(huì)重復(fù)加載。加載類的過程中,會(huì)向上問一下是否加載過,如果已經(jīng)加載了,則不會(huì)再加載,這樣可以保證一個(gè)類只會(huì)被加載一次。
- 保證類的安全性。核心的類已經(jīng)被啟動(dòng)類加載器加載了,后面即使有人篡改了該類,也不會(huì)再加載了,防止了一些有危害的代碼的植入。
3 破壞雙親委派機(jī)制的場(chǎng)景
既然Java中引入了雙親委派機(jī)制,為什么要破壞它呢?
答:因?yàn)樗幸恍┤秉c(diǎn)。
下面給大家列舉一下,破壞雙親委派機(jī)制最常見的場(chǎng)景。
3.1 JNDI
JNDI是Java中的標(biāo)準(zhǔn)服務(wù),它的代碼由啟動(dòng)類加載器去加載。
但JNDI要對(duì)資源進(jìn)行集中管理和查找,它需要調(diào)用由獨(dú)立廠商在應(yīng)用程序的ClassPath下的實(shí)現(xiàn)了JNDI接口的代碼,但啟動(dòng)類加載器不可能“認(rèn)識(shí)”這些外部代碼。
為了解決這個(gè)問題,Java后來引入了線程上下文類加載器(Thread Context ClassLoader)。
這個(gè)類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進(jìn)行設(shè)置。
如果創(chuàng)建線程時(shí)沒有設(shè)置,他將會(huì)從父線程中繼承一個(gè),如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過的話,那這個(gè)類加載器默認(rèn)就是應(yīng)用程序類加載器。
有了線程上下文加載器,JNDI服務(wù)就可以使用它去加載所需要的SPI代碼,也就是父類加載器請(qǐng)求子類加載器去完成類加載的動(dòng)作,這樣就打破了雙親委派機(jī)制。
3.2 JDBC
原生的JDBC中Driver驅(qū)動(dòng)本身只是一個(gè)接口,并沒有具體的實(shí)現(xiàn),具體的實(shí)現(xiàn)是由不同數(shù)據(jù)庫類型去實(shí)現(xiàn)的。
例如,MySQL的mysql-connector.jar中的Driver類具體實(shí)現(xiàn)的。
原生的JDBC中的類是放在rt.jar包,是由啟動(dòng)類加載器進(jìn)行類加載的。
在JDBC中需要?jiǎng)討B(tài)去加載不同數(shù)據(jù)庫類型的Driver實(shí)現(xiàn)類,而mysql-connector.jar中的Driver實(shí)現(xiàn)類是用戶自己寫的代碼,啟動(dòng)類加載器肯定是不能加載的,那就需要由應(yīng)用程序啟動(dòng)類去進(jìn)行類加載。
為了解決這個(gè)問題,也可以使用線程上下文類加載器(Thread Context ClassLoader)。
3.3 Tomcat容器
Tomcat是Servlet容器,它負(fù)責(zé)加載Servlet相關(guān)的jar包。
此外,Tomcat本身也是Java程序,也需要加載自身的類和一些依賴jar包。
這樣就會(huì)帶來下面的問題:
- 一個(gè)Tomcat容器下面,可以部署多個(gè)基于Servlet的Web應(yīng)用,但如果這些Web應(yīng)用下有同名的Servlet類,又不能產(chǎn)生沖突,需要相互獨(dú)立加載和運(yùn)行才行。
- 但如果多個(gè)Web應(yīng)用,使用了相同的依賴,比如:SpringBoot、Mybatis等。這些依賴包所涉及的文件非常多,如果全部都獨(dú)立,可能會(huì)導(dǎo)致JVM內(nèi)存不足。也就是說,有些公共的依賴包,最好能夠只加載一次。
- 我們還需要將Tomcat本身的類,跟Web應(yīng)用的類隔離開。
這些原因?qū)е?,Tomcat沒有辦法使用傳統(tǒng)的雙親委派機(jī)制加載類了。
那么,Tomcat加載類的機(jī)制是怎么樣的?
圖片
- CommonClassLoader:是Tomcat最基本的類加載器,它加載的類可以被Tomcat容器和Web應(yīng)用訪問。
- CatalinaClassLoader:是Tomcat容器私有的類加載器,加載類對(duì)于Web應(yīng)用不可見。
- SharedClassLoader:各個(gè)Web應(yīng)用共享的類加載器,加載的類對(duì)于所有Web應(yīng)用可見,但是對(duì)于Tomcat容器不可見。
- WebAppClassLoader:各個(gè)Web應(yīng)用私有的類加載器,加載類只對(duì)當(dāng)前Web應(yīng)用可見。比如不同war包應(yīng)用引入了不同的Spring版本,這樣能加載各自的Spring版本,相互隔離。
3.4 熱部署
由于用戶對(duì)程序動(dòng)態(tài)性的追求,比如:代碼熱部署、代碼熱替換等功能,引入了OSGi(Open Service Gateway Initiative)。
OSGi中的每一個(gè)模塊(稱為Bundle)。
當(dāng)程序升級(jí)或者更新時(shí),可以只停用、重新安裝然后啟動(dòng)程序的其中一部分,對(duì)企業(yè)來說這是一個(gè)非常誘人的功能。
OSGi的Bundle類加載器之間只有規(guī)則,沒有固定的委派關(guān)系。
各個(gè)Bundle加載器是平級(jí)關(guān)系。
不是雙親委派關(guān)系。