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