"深入探索:Tomcat 類(lèi)加載機(jī)制揭秘"
前言
在探究 Tomcat 類(lèi)加載機(jī)制之前,讓我們重溫一下 Java 默認(rèn)的類(lèi)加載器,加深對(duì)其的理解。如同作者在《深入理解 Java 虛擬機(jī)》第二版中所言,類(lèi)加載機(jī)制對(duì)于理解 Java 運(yùn)行時(shí)環(huán)境至關(guān)重要。
什么是類(lèi)加載機(jī)制
Java 虛擬機(jī)將描述類(lèi)的字節(jié)碼數(shù)據(jù)從 Class 文件加載至內(nèi)存,并對(duì)其進(jìn)行嚴(yán)格的校驗(yàn)、轉(zhuǎn)換解析和初始化,最終生成可供虛擬機(jī)直接執(zhí)行的 Java 類(lèi)型。這一過(guò)程便是虛擬機(jī)的類(lèi)加載機(jī)制。
虛擬機(jī)設(shè)計(jì)者將類(lèi)加載階段中“根據(jù)全限定名獲取描述類(lèi)信息的二進(jìn)制字節(jié)流”這一關(guān)鍵步驟委托給了外部實(shí)現(xiàn),賦予應(yīng)用程序自行決定如何獲取所需類(lèi)的權(quán)利。負(fù)責(zé)執(zhí)行這一任務(wù)的代碼模塊被稱(chēng)為“類(lèi)加載器”。
類(lèi)與類(lèi)加載器的關(guān)系
類(lèi)加載器雖然只負(fù)責(zé)加載類(lèi),但其影響卻遠(yuǎn)超類(lèi)加載階段本身。對(duì)于任何一個(gè)類(lèi),它與加載它的類(lèi)加載器共同決定了該類(lèi)在 Java 虛擬機(jī)中的唯一性,就好比每個(gè)類(lèi)加載器都擁有一個(gè)獨(dú)立的“類(lèi)倉(cāng)庫(kù)”,每個(gè)倉(cāng)庫(kù)中的類(lèi)都是獨(dú)一無(wú)二的。因此,判斷兩個(gè)類(lèi)是否相同,只有在它們由同一個(gè)類(lèi)加載器加載的前提下才有意義。即使兩個(gè)類(lèi)來(lái)自同一個(gè) Class 文件,被同一個(gè)虛擬機(jī)加載,只要加載它們的類(lèi)加載器不同,它們也必然被視為不同的類(lèi)。
什么是雙親委任模型
- 從 Java 虛擬機(jī)的視角來(lái)看,類(lèi)加載器僅存在兩種類(lèi)型:一是
啟動(dòng)類(lèi)加載器
(Bootstrap ClassLoader),它由 C++語(yǔ)言實(shí)現(xiàn)(僅限于 HotSpot 虛擬機(jī)),是虛擬機(jī)自身的一部分;二是所有其他類(lèi)加載器
,它們均由 Java 語(yǔ)言實(shí)現(xiàn),獨(dú)立于虛擬機(jī)外部,并且都繼承自抽象類(lèi) java.lang.ClassLoader。 - 從 Java 開(kāi)發(fā)者的角度,類(lèi)加載器可以更細(xì)致地劃分,大部分 Java 程序員會(huì)接觸到以下三種系統(tǒng)提供的類(lèi)加載器:
啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader):它負(fù)責(zé)加載位于 JAVA_HOME/lib 目錄下的,或者被-Xbootclasspath 參數(shù)指定的路徑中的,并且被虛擬機(jī)識(shí)別的類(lèi)庫(kù)(僅根據(jù)文件名識(shí)別,例如 rt.jar,其他名字的類(lèi)庫(kù)即使放在 lib 目錄下也不會(huì)被重載)。
擴(kuò)展類(lèi)加載器(Extension ClassLoader):由 sun.misc.Launcher$ExtClassLoader 實(shí)現(xiàn),它負(fù)責(zé)加載位于 JAVA_HOME/lib/ext 目錄下的,或由 java.ext.dirs 系統(tǒng)變量指定的路徑中的所有類(lèi)庫(kù)。開(kāi)發(fā)者可以直接使用擴(kuò)展類(lèi)加載器。
應(yīng)用程序類(lèi)加載器(Application ClassLoader):由 sun.misc.Launcher$AppClassLoader 實(shí)現(xiàn),由于它是 ClassLoader 中的 getSystemClassLoader 方法的返回值,因此也被稱(chēng)為系統(tǒng)類(lèi)加載器。它負(fù)責(zé)加載用戶(hù)類(lèi)路徑(ClassPath)上所指定的類(lèi)庫(kù)。開(kāi)發(fā)者可以直接使用這個(gè)類(lèi)加載器,如果應(yīng)用程序沒(méi)有自定義類(lèi)加載器,它通常是程序中的默認(rèn)類(lèi)加載器。
這些類(lèi)加載器之間的關(guān)系一般如下圖所示:
圖片
圖中各個(gè)類(lèi)加載器之間的關(guān)系被稱(chēng)為類(lèi)加載器的雙親委派模型(Parents Delegation Mode)。雙親委派模型規(guī)定,除了頂層的啟動(dòng)類(lèi)加載器之外,其他所有類(lèi)加載器都應(yīng)該由其父類(lèi)加載器加載。這里類(lèi)加載器之間的父子關(guān)系通常不通過(guò)繼承實(shí)現(xiàn),而是使用組合關(guān)系來(lái)復(fù)用父加載器的代碼。
類(lèi)加載器的雙親委派模型在 JDK 1.2 時(shí)期被引入,并被廣泛應(yīng)用于之后的 Java 程序中,但它并非強(qiáng)制性約束模型,而是 Java 設(shè)計(jì)者推薦給開(kāi)發(fā)者的一種類(lèi)加載器實(shí)現(xiàn)方式。
雙親委派模型的工作流程如下:當(dāng)一個(gè)類(lèi)加載器收到類(lèi)加載請(qǐng)求時(shí),它不會(huì)立即嘗試加載該類(lèi),而是將請(qǐng)求委托給父類(lèi)加載器處理。每一層級(jí)類(lèi)加載器都遵循這一原則,最終請(qǐng)求將傳遞到頂層的啟動(dòng)類(lèi)加載器。只有當(dāng)父加載器反饋無(wú)法完成請(qǐng)求(在其搜索范圍內(nèi)沒(méi)有找到所需的類(lèi))時(shí),子加載器才會(huì)嘗試自己加載。
為什么要使用雙親委派模型
如果沒(méi)有使用雙親委派模型,而是由各個(gè)類(lèi)加載器自行加載類(lèi),那么如果用戶(hù)編寫(xiě)了一個(gè)名為java.lang.Object
的類(lèi)并將其放置在程序的 ClassPath 中,系統(tǒng)中就會(huì)出現(xiàn)多個(gè)不同的 Object 類(lèi)。Java 類(lèi)型體系中最基礎(chǔ)的行為將無(wú)法保證,應(yīng)用程序也將變得混亂不堪。
雙親委任模型時(shí)如何實(shí)現(xiàn)的
非常簡(jiǎn)單,雙親委派模型的核心邏輯體現(xiàn)在 java.lang.ClassLoader 中的 loadClass 方法中。
圖片
首先判斷若類(lèi)尚未加載,則委派父加載器嘗試加載。父加載器為空時(shí),則默認(rèn)委托啟動(dòng)類(lèi)加載器。若父加載器加載失敗,則拋出 ClassNotFoundException 異常,隨后調(diào)用自定義 findClass 方法進(jìn)行加載。
如何破壞雙親委任模型
雙親委派模型并非強(qiáng)制性約束,而是 Java 設(shè)計(jì)者推薦的類(lèi)加載器實(shí)現(xiàn)方式。雖然大部分類(lèi)加載器都遵循這一模型,但也有例外。迄今為止,雙親委派模型曾三次被“打破”。
第一次發(fā)生在雙親委派模型出現(xiàn)之前,即 JDK 1.2 發(fā)布之前。
第二次則是模型本身的缺陷所致。雙親委派模型有效地解決了基礎(chǔ)類(lèi)的統(tǒng)一加載問(wèn)題(越基礎(chǔ)的類(lèi)由越上層的加載器加載),然而,并非所有基礎(chǔ)類(lèi)都只被用戶(hù)代碼調(diào)用。如果基礎(chǔ)類(lèi)需要調(diào)用用戶(hù)代碼,就會(huì)出現(xiàn)問(wèn)題。
這并非不可能。JNDI 服務(wù)就是一個(gè)典型例子。作為 Java 的標(biāo)準(zhǔn)服務(wù),JNDI 的代碼由啟動(dòng)類(lèi)加載器加載(在 JDK 1.3 時(shí)就已包含在 rt.jar 中),但它需要調(diào)用獨(dú)立廠商實(shí)現(xiàn)并部署在應(yīng)用程序 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface)的代碼。然而,啟動(dòng)類(lèi)加載器無(wú)法“識(shí)別”這些代碼,因?yàn)樗鼈儾⒉辉?rt.jar 中。為了解決這個(gè)問(wèn)題,啟動(dòng)類(lèi)加載器需要加載這些代碼。
為了解決這個(gè)問(wèn)題,Java 設(shè)計(jì)團(tuán)隊(duì)引入了一個(gè)名為線(xiàn)程上下文類(lèi)加載器(Thread Context ClassLoader)的設(shè)計(jì)。這個(gè)類(lèi)加載器可以通過(guò) java.lang.Thread 類(lèi)的 setContextClassLoader 方法進(jìn)行設(shè)置。如果在創(chuàng)建線(xiàn)程時(shí)尚未設(shè)置,它會(huì)從父線(xiàn)程中繼承一個(gè);如果在應(yīng)用程序的全局范圍內(nèi)都沒(méi)有設(shè)置,那么這個(gè)類(lèi)加載器默認(rèn)就是應(yīng)用程序類(lèi)加載器。
有了線(xiàn)程上下文加載器,JNDI 服務(wù)便可以使用它來(lái)加載所需的 SPI 代碼。這相當(dāng)于父類(lèi)加載器請(qǐng)求子類(lèi)加載器完成類(lèi)加載,打破了雙親委派模型的層次結(jié)構(gòu),逆向使用類(lèi)加載器,實(shí)際上已經(jīng)違背了模型的一般性原則。但這是無(wú)奈之舉,Java 中所有涉及 SPI 加載的動(dòng)作基本上都采用這種方式,例如 JNDI、JDBC、JCE、JAXB、JBI 等。
第三次破壞則是為了實(shí)現(xiàn)熱插拔、熱部署、模塊化。這意味著添加或刪除功能無(wú)需重啟,只需將模塊連同其類(lèi)加載器一起替換,即可實(shí)現(xiàn)代碼熱替換。
Tomcat 的類(lèi)加載器是怎么設(shè)計(jì)的
首先,我們來(lái)思考個(gè)問(wèn)題:
Tomcat 如果使用默認(rèn)的類(lèi)加載機(jī)制行不行?
細(xì)細(xì)想一下,Tomcat 作為一款 Web 容器,其存在的意義何在?到底是為了解決怎樣的問(wèn)題?
- Web 容器或需承載多個(gè)應(yīng)用程序,而不同應(yīng)用可能依賴(lài)于同一第三方類(lèi)庫(kù)的不同版本。為確保應(yīng)用間相互隔離,每個(gè)應(yīng)用程序的類(lèi)庫(kù)應(yīng)保持獨(dú)立,避免彼此干擾。
- 同一 Web 容器中的相同類(lèi)庫(kù)版本可共享,以避免資源浪費(fèi)。若每個(gè)應(yīng)用程序都獨(dú)立加載相同類(lèi)庫(kù),則當(dāng)服務(wù)器承載十個(gè)應(yīng)用程序時(shí),將會(huì)加載十份相同的類(lèi)庫(kù),這無(wú)疑是極不合理的。
- Web 容器自身亦有其依賴(lài)的類(lèi)庫(kù),不可與應(yīng)用程序的類(lèi)庫(kù)混淆。出于安全考慮,容器的類(lèi)庫(kù)與應(yīng)用程序的類(lèi)庫(kù)應(yīng)嚴(yán)格隔離,互不干擾。
- Web 容器需具備對(duì) JSP 文件修改的支持。眾所周知,JSP 文件最終需編譯成 Class 文件才能在虛擬機(jī)中運(yùn)行。然而,程序運(yùn)行后修改 JSP 文件已成常態(tài),否則容器便無(wú)實(shí)際意義。因此,Web 容器應(yīng)支持 JSP 修改后無(wú)需重啟服務(wù)器,以提高開(kāi)發(fā)效率。
再回頭看問(wèn)題,Tomcat 如果使用默認(rèn)的類(lèi)加載機(jī)制行不行?
答案是不行的。為什么?
首先,默認(rèn)的類(lèi)加載器機(jī)制無(wú)法加載相同類(lèi)庫(kù)的不同版本。其機(jī)制只關(guān)注全限定類(lèi)名,而不會(huì)區(qū)分版本。因此,第一個(gè)和第三個(gè)問(wèn)題無(wú)法通過(guò)默認(rèn)機(jī)制解決。
其次,默認(rèn)類(lèi)加載器的職責(zé)正是確保類(lèi)庫(kù)的唯一性,這與第二個(gè)問(wèn)題并不沖突。
至于第四個(gè)問(wèn)題,熱修改 JSP 文件面臨挑戰(zhàn)。JSP 文件最終編譯成 Class 文件,修改后的 JSP 文件仍擁有相同的類(lèi)名,導(dǎo)致類(lèi)加載器直接從方法區(qū)中獲取已存在的 Class 文件,無(wú)法加載修改后的內(nèi)容。
為了解決這個(gè)問(wèn)題,可以為每個(gè) JSP 文件創(chuàng)建唯一的類(lèi)加載器。當(dāng) JSP 文件修改后,直接卸載該類(lèi)加載器,并重新創(chuàng)建類(lèi)加載器,從而重新加載修改后的 JSP 文件。
Tomcat 如何實(shí)現(xiàn)自己獨(dú)特的類(lèi)加載機(jī)制
首先看下 Tomcat 的設(shè)計(jì)圖:
圖片
觀察這張圖,我們看到了多個(gè)類(lèi)加載器,其中除了 JDK 自帶的類(lèi)加載器之外,我們尤其關(guān)注 Tomcat 自身持有的類(lèi)加載器。細(xì)細(xì)觀察,我們會(huì)發(fā)現(xiàn) Catalina 類(lèi)加載器和 Shared 類(lèi)加載器并非父子關(guān)系,而是兄弟關(guān)系。這種設(shè)計(jì)背后的緣由,需要我們分析每個(gè)類(lèi)加載器的用途才能明了。
從圖中我們能了解到 Tomcat 類(lèi)加載器體系結(jié)構(gòu)的設(shè)計(jì)精妙,每個(gè)類(lèi)加載器各司其職,確保了系統(tǒng)的穩(wěn)定性和安全性。
- Common 類(lèi)加載器 負(fù)責(zé)加載 Tomcat 和 Web 應(yīng)用共同復(fù)用的類(lèi),例如日志框架、通用工具庫(kù)等。
- Catalina 類(lèi)加載器 專(zhuān)注于加載 Tomcat 自身的類(lèi),這些類(lèi)在 Web 應(yīng)用中不可見(jiàn),確保了 Tomcat 核心功能的獨(dú)立性。
- Shared 類(lèi)加載器 負(fù)責(zé)加載所有 Web 應(yīng)用共同復(fù)用的類(lèi),例如數(shù)據(jù)庫(kù)連接池、緩存框架等,這些類(lèi)在 Tomcat 中不可見(jiàn),避免了應(yīng)用之間的沖突。
- WebApp 類(lèi)加載器 為每個(gè) Web 應(yīng)用單獨(dú)創(chuàng)建,負(fù)責(zé)加載該應(yīng)用的類(lèi),這些類(lèi)在 Tomcat 和其他應(yīng)用中不可見(jiàn),確保了應(yīng)用之間的隔離。
- Jsp 類(lèi)加載器 為每個(gè) JSP 頁(yè)面創(chuàng)建唯一的類(lèi)加載器,方便實(shí)現(xiàn) JSP 頁(yè)面的熱插拔,提高開(kāi)發(fā)效率。
至此,我們對(duì) Tomcat 類(lèi)加載器體系有了初步了解,接下來(lái)將深入探討其源碼實(shí)現(xiàn)。由于篇幅所限,詳細(xì)分析將在下一篇文章中展開(kāi)。