iT邦幫忙

2026 iThome 鐵人賽

0
Build on Google AI

把Google AI當夥伴打造個人高效Coding工作流系列 第 25 篇

第二十五天雙親委派模型、打破雙親委派與熱部署實踐

  • 分享至 

  • xImage
  •  

一、 類別生命週期全貌:從 .class 到記憶體元空間

  • 載入:透過全限定類名(FQCN)抓取二進位字節流,在記憶體中生成對應的 java.lang.Class 物件。
  • 驗證:確保字節碼符合 JVM 規範,防禦惡意代碼破壞系統(檔案格式、元數據、字節碼安全驗證)。
  • 準備:在方法區(元空間 Metaspace)為 static 類別變數配置記憶體,並賦予型別預設零值(如 int 給 0、引用型別給 null,此時尚未執行賦值語句;但 static final 常數會在此階段直接被賦予真實值)。
  • 解析:將常數池內的符號引用(Symbolic References)轉為記憶體實體位址的直接引用(Direct References)。
  • 初始化:正式執行類別建構器 () 方法(由靜態變數顯式賦值語句與 static {} 靜態代碼塊按宣告順序合併而成)。
  • 使用。
  • 卸載。

二、 類別載入器層級與雙親委派模型(Parent Delegation Model)
JVM 內建了具備層級關係的類別載入器體系:

  • Bootstrap ClassLoader(啟動類別載入器):
    • 由 C/C++ 撰寫,嵌入在 JVM 核心內部,Java 代碼中調用其引用會返回 null。
    • 負責載入 <JAVA_HOME>/lib 目錄下的核心類庫(如 rt.jar、java.lang.*)。
  • Platform ClassLoader(平台類別載入器,JDK 8 稱為 Extension):
    • 負責載入擴充目錄(如 <JAVA_HOME>/lib/ext)中的擴充類別庫。
  • Application ClassLoader(應用程式類別載入器):
    • 負責載入使用者類別路徑(Classpath)上的所有第三方依賴 Jar 包與自己寫的業務 Class。
  • 雙親委派運作機制:
    • 「自底向上檢查快取,自頂向下嘗試載入」。
    • 當一個 ClassLoader 收到載入請求時,它絕不會自己先去載入,而是將請求一層層委派給父載體(Parent),直到最頂層的 Bootstrap ClassLoader。
    • 只有當父載體回報「在自己的搜尋路徑中找不到該類別(拋出 ClassNotFoundException)」時,子載體才會嘗試自己去載入。

三、 為什麼需要雙親委派?核心安全防線

  • 避免類別重複載入:父載入器已經載入過的 Class,子載入器直接複用快取,保證記憶體中類別定義的唯一性。
  • 保護 Java 核心 API 的沙箱安全(防篡改):
    • 假設惡意攻擊者自訂了一個 java.lang.String,並在裡面植入後門代碼。
    • 由於雙親委派機制,請求會一路委派至 Bootstrap ClassLoader,頂層直接載入了 JDK 原生的安全 String,黑客寫的偽造類別永遠沒有機會被載入進記憶體執行。
  • loadClass() vs findClass()
    • 遵守雙親委派:自訂 ClassLoader 時,只覆寫 findClass()(實現自己的讀取字節碼邏輯),保留 loadClass() 的父委派骨架。
    • 打破雙親委派:自訂 ClassLoader 時,直接覆寫 loadClass(),改變原本由父先載入的流程!

四、歷史上三次打破雙親委派的架構實踐
打破雙親委派並非背叛規範,而是為了滿足現實工程架構的強烈需求:

  1. 第一次打破:向前相容歷史債(JDK 1.2)
  • ClassLoader 在 JDK 1.0 就存在,當時只有 loadClass()。JDK 1.2 引入雙親委派時,為了相容老程式碼,官方才妥協新增了 findClass(),並建議後人只覆寫 findClass()。
  1. 第二次打破:基礎核心類別需要調用用戶代碼(SPI 機制,如 JDBC)
  • 架構死局:
    • JDBC 介面規範(java.sql.Driver)位於核心庫,由頂層 Bootstrap ClassLoader 載入。
    • 具體的廠商驅動實作(如 com.mysql.cj.jdbc.Driver)放在第三方 Jar,屬於 Application ClassLoader 的管轄範圍。
    • 核心類的代碼需要呼叫外部廠商的實作類,但頂層的 Bootstrap ClassLoader 根本「看不到」下層的 MySQL 驅動!
  • 破局解法:執行緒上下文類別載入器(Thread Context ClassLoader, TCCL):
    • Java 引入了 Thread.currentThread().getContextClassLoader()。
    • Bootstrap ClassLoader 透過借用主執行緒設定好的 Application ClassLoader,強行將載入任務委派給下層載入器完成,打破了嚴格「只能由下往上」的委派限制。
  1. 第三次打破:Web 容器架構隔離需求(Tomcat)
  • Tomcat 伺服器必須同時運行多個獨立的 Web 應用(WAR 包),面臨以下挑戰:
    • 應用隔離:App 1 依賴 Spring 5,App 2 依賴 Spring 6,同一個 JVM 必須同時載入同名的類別而不互相干擾。
    • 容器自保:Web 應用內部的 Class 不能篡改 Tomcat 本身的核心類。
  • Tomcat 的類別載入架構設計:
    • 打破規則的行為:WebAppClassLoader 徹底覆寫了 loadClass()。
    • 載入優先順序:收到載入請求時,先在應用自己的 /WEB-INF/classes 與 /WEB-INF/lib 搜尋,自己找不到才向上委派給父載體(除了 java.* 核心類仍強制委派給 Bootstrap 外),從而達成各 Web 應用間相同全限定名類別的完美物理隔離。

五、 熱部署與類別卸載實作原理

  • 在不停機的情況下替換業務邏輯,其底層全靠ClassLoader的生命週期管理:
    • Class卸載的苛刻條件:
    • 該類別在堆積中的所有實例對象都已被GC回收。
    • 載入該類別的 ClassLoader 實例本身已經不可達且被GC回收。
    • 該類別對應的 java.lang.Class 物件沒有在任何地方被引用。
  • 熱替換核心實務步驟:
    • 建立一個專門載入業務外掛模組的自訂ClassLoader實例 $L_1$。
    • 透過 $L_1$ 載入PluginV1.class並運行。
    • 當程式碼更新成PluginV2.class時,直接切斷對 $L_1$ 的所有引用。
    • 重新new一個全新的ClassLoader實例 $L_2$,用 $L_2$ 載入新版字節碼,舊的 $L_1$ 與 V1 Class隨後在下一次GC中被徹底卸載。

上一篇
第二十四天BIO、NIO、Selector多路複用與Netty底層核心機制
系列文
把Google AI當夥伴打造個人高效Coding工作流 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言