4.2 方法式 JIT 與熱點偵測(HotSpot)#
HotSpot 的誕生:把 Self 的理論變成產品#
1990 年代中期,Sun 的 Java 直譯器效能成為 Java 晉級伺服器主力語言的最大障礙。Sun 先後嘗試過多個 JIT 專案(如 1998 年 JDK 1.2 的 Symantec JIT),真正的轉折是 1997 年收購了 Longview Technologies——一家由 Smalltalk 老兵(包括 Lars Bak)創立的小公司,其產品正是以 Self 研究為基礎的 Java VM。這個 VM 在 2001 年的 JDK 1.3 中成為 Sun 的預設 JVM,取名 HotSpot——名字來自其核心主張:不要平均用力編譯整個程式,只編譯「熱點(hot spot)」。與之並行的還有由 IBM 主導(Cliff Click 等人,2000 年前後)的 Sea of Nodes 式伺服器編譯器研究,深刻影響了 HotSpot 的 C2 編譯器設計(Cliff Click 的 sea-of-nodes IR 讓優化與排程解耦,成為現代編譯器 IR 的經典設計)。
熱點偵測:取樣 vs 計數#
方法式 JIT 的第一步是決定「編譯誰」。兩種主流偵測機制:
- 取樣式(sampling):由一條低優先權的背景執行緒週期性地取樣所有執行緒的 PC,落在某方法/某迴圈的次數累積到門檻即判定為熱點。優點是近乎零開銷、自然反映真實時間分佈;缺點是雜訊大、對短命熱點不靈敏。
- 計數式(counter-based):在每個方法入口與每個迴圈回邊(backedge)插入計數器,每次執行遞增,超過門檻(如 HotSpot 預設方法約一萬次、迴圈約數千次回邊,且隨分層模式而變)即觸發編譯。優點是精確、可調;缺點是計數指令本身有開銷,且門檻需要調參。
HotSpot 採用計數式為主(-XX:CompileThreshold 可調),並有一個關鍵巧思——on-stack replacement(OSR,堆疊上替換):如果熱點是一個正在執行的長迴圈(方法還沒返回),編譯器需能把「已執行到迴圈中段的直譯框架」直接替換成「新編譯碼的框架」。OSR 機制(1990 年代在 Self 與 IBM 研究中成形)為每個回邊位置建立一個特殊的 OSR 入口,把直譯器的區域變數與運算堆疊狀態映射到編譯碼的暫存器/堆疊位置。沒有 OSR,「第一次執行就跑十分鐘的迴圈」這種 worst case 會讓 JIT 完全失效。
編譯管線與分層編譯#
HotSpot 的編譯管線以方法為單位:
位元碼 ──前端──> 中間表示(C1: 線性IR / C2: Sea of Nodes SSA)
──優化──> 內聯、逃逸分析、常數傳播、範圍檢查刪除…
──後端──> 機器碼(含去優化點、守衛、GC safepoint)
JDK 8(2014)起預設啟用分層編譯(tiered compilation),把執行引擎分成五層:
- Tier 0:直譯器
- Tier 1:C1(client 編譯器)無 profiling 的快速編譯
- Tier 2/3:C1 + 各種 profiling(計數、型別觀測)
- Tier 4:C2(server 編譯器)完全優化
分層的邏輯是期望成本優化:直譯器零啟動成本;C1 編譯快(毫秒級)但只做輕量優化;C2 編譯慢(可達數十至數百毫秒)但碼品質高。先用 C1 墊檔並收集 profile,等確定是長命熱點才投入 C2。這個「由便宜到昂貴、由保守到激進」的階梯,正是 4.1 節「優化成本不對稱」理論的工程化。JavaScript 引擎(V8 的 Ignition→Sparkplug→Maglev→TurboFan,2021-2023 年間逐層補齊)幾乎複刻了同一架構,可見此架構已成共識。
樂觀優化與去優化:動態世界的保險絲#
C2 對 Java 這種「理論上靜態」的語言仍需大量推測:invokevirtual 的接收者可能有多型態(需 inline cache,見 4.3)、類別可以動態載入(可能推翻既有假設,如「此呼叫點只有一個實作」)、欄位存取假設的位移可能因 subclass 的欄位重排而變。因此編譯碼中布滿守衛:
if (接收者型別 == Point) { 直連的 point.x 碼 }
else { 呼叫去優化常式 -> 退回直譯器/低層碼 }
去優化時,VM 必須重建直譯框架:編譯時為每個去優化點記錄「每個虛擬暫存器的值來自哪個機器位置(或已消失、需物化)」,執行期據此把機器碼狀態轉換回位元碼狀態。此外,JIT 還要與 GC 合作:編譯碼中必須標記 safepoint(安全點),使 GC 能在任意時刻於已知的棧幀中找到所有指標——這是 JIT 與 GC 的介面合約,也是精確 GC 得以在編譯碼上運作的前提。
與編譯器/OS 領域的關聯#
方法式 JIT 本質上是一個「以執行期為單位增量運作的 AOT 編譯器」,其 IR、優化演算法(內聯、逃逸分析、範圍檢查消除)直接取材自編譯器課程;但它新增了 AOT 沒有的三個工程問題:編譯預算(編譯時間是執行時間的一部分,因此編譯器自身也要快)、可逆性(去優化路徑的正確性)、與 W^X 記憶體管理(產生的機器碼放進可執行頁面,現代 OS 要求寫/執行權限分離,VM 需以 mprotect 或雙映射技巧管理 code cache)。從作業系統視角,JIT 的 code cache 就是一個「自我產生程式碼的用戶空間記憶體區域」,其頁面管理、快取失效(指令快取一致性,ARM 架構上需要明確 flush icache)都是真實的系統程式議題。
小結#
HotSpot 把「熱點偵測 → 分層編譯 → 樂觀優化 → 守衛與去優化」整合成一套可維運的產品架構,使 Java 效能達到接近 C 的水準,也確立了此後二十年 VM 執行引擎的標準藍圖。下一節深入其中最精妙的單項技術:源自 Self 的內嵌快取與形態推測。