前言
在探究 Tomcat 類載入機制之前,讓我們重溫一下 Java 預設的類載入器,加深對其的理解。 如同作者在《深入理解 Java 虛擬機器》第二版中所言,類載入機制對於理解 Java 執行時環境至關重要。
什麼是類載入機制
Java 虛擬機器將描述類的位元組碼資料從 Class 檔案載入至記憶體,並對其進行嚴格的校驗、轉換解析和初始化,最終生成可供虛擬機器直接執行的 Java 型別。這一過程便是虛擬機器的類載入機制。
虛擬機器設計者將類載入階段中“根據全限定名獲取描述類資訊的二進制位元組流”這一關鍵步驟委託給了外部實現,賦予應用程式自行決定如何獲取所需類的權利。負責執行這一任務的程式碼模組被稱為“類載入器”。
類與類載入器的關係
類載入器雖然只負責載入類,但其影響卻遠超類載入階段本身。對於任何一個類,它與載入它的類載入器共同決定了該類在 Java 虛擬機器中的唯一性,就好比每個類載入器都擁有一個獨立的“類倉庫”,每個倉庫中的類都是獨一無二的。因此,判斷兩個類是否相同,只有在它們由同一個類載入器載入的前提下才有意義。即使兩個類來自同一個 Class 檔案,被同一個虛擬機器載入,只要載入它們的類載入器不同,它們也必然被視為不同的類。
什麼是雙親委任模型
從 Java 虛擬機器的視角來看,類載入器僅存在兩種型別:一是
啟動類載入器
(Bootstrap ClassLoader),它由 C++語言實現(僅限於 HotSpot 虛擬機器),是虛擬機器自身的一部分;二是所有其他類載入器
,它們均由 Java 語言實現,獨立於虛擬機器外部,並且都繼承自抽象類 java.lang.ClassLoader。從 Java 開發者的角度,類載入器可以更細緻地劃分,大部分 Java 程式設計師會接觸到以下三種系統提供的類載入器:
啟動類載入器(Bootstrap ClassLoader):它負責載入位於 JAVA_HOME/lib 目錄下的,或者被-Xbootclasspath 引數指定的路徑中的,並且被虛擬機器識別的類庫(僅根據檔名識別,例如 rt.jar,其他名字的類庫即使放在 lib 目錄下也不會被過載)。
擴充套件類載入器(Extension ClassLoader):由 sun.misc.Launcher$ExtClassLoader 實現,它負責載入位於 JAVA_HOME/lib/ext 目錄下的,或由 java.ext.dirs 系統變數指定的路徑中的所有類庫。開發者可以直接使用擴充套件類載入器。
應用程式類載入器(Application ClassLoader):由 sun.misc.Launcher$AppClassLoader 實現,由於它是 ClassLoader 中的 getSystemClassLoader 方法的返回值,因此也被稱為系統類載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫。開發者可以直接使用這個類載入器,如果應用程式沒有自定義類載入器,它通常是程式中的預設類載入器。
這些類載入器之間的關係一般如下圖所示:
圖中各個類載入器之間的關係被稱為類載入器的雙親委派模型(Parents Delegation Mode)。雙親委派模型規定,除了頂層的啟動類載入器之外,其他所有類載入器都應該由其父類載入器載入。這裏類載入器之間的父子關係通常不透過繼承實現,而是使用組合關係來複用父載入器的程式碼。
類載入器的雙親委派模型在 JDK 1.2 時期被引入,並被廣泛應用於之後的 Java 程式中,但它並非強制性約束模型,而是 Java 設計者推薦給開發者的一種類載入器實現方式。
雙親委派模型的工作流程如下:當一個類載入器收到類載入請求時,它不會立即嘗試載入該類,而是將請求委託給父類載入器處理。每一層級類載入器都遵循這一原則,最終請求將傳遞到頂層的啟動類載入器。只有當父載入器反饋無法完成請求(在其搜索範圍內沒有找到所需的類)時,子載入器纔會嘗試自己載入。
為什麼要使用雙親委派模型
如果沒有使用雙親委派模型,而是由各個類載入器自行載入類,那麼如果使用者編寫了一個名為java.lang.Object
的類並將其放置在程式的 ClassPath 中,系統中就會出現多個不同的 Object 類。Java 型別體系中最基礎的行為將無法保證,應用程式也將變得混亂不堪。
雙親委任模型時如何實現的
非常簡單,雙親委派模型的核心邏輯體現在 java.lang.ClassLoader 中的 loadClass 方法中。
首先判斷若類尚未載入,則委派父載入器嘗試載入。父載入器為空時,則預設委託啟動類載入器。若父載入器載入失敗,則丟擲 ClassNotFoundException 異常,隨後呼叫自定義 findClass 方法進行載入。
如何破壞雙親委任模型
雙親委派模型並非強制性約束,而是 Java 設計者推薦的類載入器實現方式。雖然大部分類載入器都遵循這一模型,但也有例外。迄今為止,雙親委派模型曾三次被“打破”。
第一次發生在雙親委派模型出現之前,即 JDK 1.2 釋出之前。
第二次則是模型本身的缺陷所致。雙親委派模型有效地解決了基礎類的統一載入問題(越基礎的類由越上層的載入器載入),然而,並非所有基礎類都只被使用者程式碼呼叫。如果基礎類需要呼叫使用者程式碼,就會出現問題。
這並非不可能。JNDI 服務就是一個典型例子。作為 Java 的標準服務,JNDI 的程式碼由啟動類載入器載入(在 JDK 1.3 時就已包含在 rt.jar 中),但它需要呼叫獨立廠商實現並部署在應用程式 ClassPath 下的 JNDI 介面提供者(SPI,Service Provider Interface)的程式碼。然而,啟動類載入器無法“識別”這些程式碼,因為它們並不在 rt.jar 中。爲了解決這個問題,啟動類載入器需要載入這些程式碼。
爲了解決這個問題,Java 設計團隊引入了一個名為執行緒上下文類載入器(Thread Context ClassLoader)的設計。這個類載入器可以透過 java.lang.Thread 類的 setContextClassLoader 方法進行設定。如果在建立執行緒時尚未設定,它會從父執行緒中繼承一個;如果在應用程式的全域性範圍內都沒有設定,那麼這個類載入器預設就是應用程式類載入器。
有了執行緒上下文載入器,JNDI 服務便可以使用它來載入所需的 SPI 程式碼。這相當於父類載入器請求子類載入器完成類載入,打破了雙親委派模型的層次結構,逆向使用類載入器,實際上已經違背了模型的一般性原則。但這是無奈之舉,Java 中所有涉及 SPI 載入的動作基本上都採用這種方式,例如 JNDI、JDBC、JCE、JAXB、JBI 等。
第三次破壞則是爲了實現熱插拔、熱部署、模組化。這意味著新增或刪除功能無需重啟,只需將模組連同其類載入器一起替換,即可實現程式碼熱替換。
Tomcat 的類載入器是怎麼設計的
首先,我們來思考個問題:
Tomcat 如果使用預設的類載入機制行不行?
細細想一下,Tomcat 作為一款 Web 容器,其存在的意義何在? 到底是爲了解決怎樣的問題?
Web 容器或需承載多個應用程式,而不同應用可能依賴於同一第三方類庫的不同版本。為確保應用間相互隔離,每個應用程式的類庫應保持獨立,避免彼此干擾。
同一 Web 容器中的相同類庫版本可共享,以避免資源浪費。若每個應用程式都獨立載入相同類庫,則當伺服器承載十個應用程式時,將會載入十份相同的類庫,這無疑是極不合理的。
Web 容器自身亦有其依賴的類庫,不可與應用程式的類庫混淆。出於安全考慮,容器的類庫與應用程式的類庫應嚴格隔離,互不干擾。
Web 容器需具備對 JSP 檔案修改的支援。衆所周知,JSP 檔案最終需編譯成 Class 檔案才能在虛擬機器中執行。然而,程式執行後修改 JSP 檔案已成常態,否則容器便無實際意義。因此,Web 容器應支援 JSP 修改後無需重啟伺服器,以提高開發效率。
再回頭看問題,Tomcat 如果使用預設的類載入機制行不行?
答案是不行的。為什麼?
首先,預設的類載入器機制無法載入相同類庫的不同版本。其機制只關注全限定類名,而不會區分版本。因此,第一個和第三個問題無法透過預設機制解決。
其次,預設類載入器的職責正是確保類庫的唯一性,這與第二個問題並不衝突。
至於第四個問題,熱修改 JSP 檔案面臨挑戰。 JSP 檔案最終編譯成 Class 檔案,修改後的 JSP 檔案仍擁有相同的類名,導致類載入器直接從方法區中獲取已存在的 Class 檔案,無法載入修改後的內容。
爲了解決這個問題,可以為每個 JSP 檔案建立唯一的類載入器。當 JSP 檔案修改後,直接解除安裝該類載入器,並重新建立類載入器,從而重新載入修改後的 JSP 檔案。
Tomcat 如何實現自己獨特的類載入機制
首先看下 Tomcat 的設計圖:
觀察這張圖,我們看到了多個類載入器,其中除了 JDK 自帶的類載入器之外,我們尤其關注 Tomcat 自身持有的類載入器。細細觀察,我們會發現 Catalina 類載入器和 Shared 類載入器並非父子關係,而是兄弟關係。這種設計背後的緣由,需要我們分析每個類載入器的用途才能明瞭。
從圖中我們能瞭解到 Tomcat 類載入器體系結構的設計精妙,每個類載入器各司其職,確保了系統的穩定性和安全性。
Common 類載入器 負責載入 Tomcat 和 Web 應用共同複用的類,例如日誌框架、通用工具庫等。
Catalina 類載入器 專注於載入 Tomcat 自身的類,這些類在 Web 應用中不可見,確保了 Tomcat 核心功能的獨立性。
Shared 類載入器 負責載入所有 Web 應用共同複用的類,例如資料庫連線池、快取框架等,這些類在 Tomcat 中不可見,避免了應用之間的衝突。
WebApp 類載入器 為每個 Web 應用單獨建立,負責載入該應用的類,這些類在 Tomcat 和其他應用中不可見,確保了應用之間的隔離。
Jsp 類載入器 為每個 JSP 頁面建立唯一的類載入器,方便實現 JSP 頁面的熱插拔,提高開發效率。
至此,我們對 Tomcat 類載入器體系有了初步瞭解,接下來將深入探討其原始碼實現。由於篇幅所限,詳細分析將在下一篇文章中展開。