一、概述
即時編譯器(Just In Time Compiler),也稱為 JIT 編譯器,它的主要工作是把熱點代碼編譯成與本地平台相關的機器碼,并進行各種層次的優化,進而提高代碼執行的效率。
那麼什麼是熱點代碼呢?我們知道虛拟機通過解釋器(Interpreter)來執行位元組碼檔案,當虛拟機發現某個方法或代碼塊的運作特别頻繁時,就會把這些代碼認定為“熱點代碼”(Hot Spot Code)。
即時編譯器編譯性能的好壞、代碼優化程度的高低是衡量一款商用虛拟機優秀與否的關鍵名額之一,它也是虛拟機最核心且最能展現技術水準的部分。
然而,程式員在開發過程中,壓根不會感覺到即時編譯器的存在,也參與不了即時編譯器的過程,是以我們對即時編譯器的學習更多的是了解,明白怎麼寫代碼才能更好的被即時編譯器優化。
二、工作流程
HotSpot 虛拟機包含解釋器和編譯器。它們是怎麼搭配工作的呢?當程式啟動的時候,解釋器首先發揮作用,它能直接運作位元組碼檔案;随着時間的推移,越來越多的熱點代碼被編譯器編譯成機器碼,進而擷取更高的執行效率。同時,解釋器還可以作為編譯器激進優化時的一個“逃生門”,當編譯器的激進優化手段不成立時,如加載了新類後類型繼承結構出現變化等,可以通過逆優化(Deoptimization)退回到解釋狀态繼續由解釋器執行。
編譯器又分為兩種,C1 編譯器(Client Compiler)和 C2 編譯器(Server Compiler),HotSpot 虛拟機會選擇哪個編譯器是由虛拟機運作于 Client 模式還是 Server 模式決定的。

預設情況下,虛拟機采用解釋器和一種編譯器搭配的方式工作,但是在分層編譯政策下,C1 編譯器和 C2 編譯器将會同時工作,分層編譯根據編譯器編譯、優化的規模和耗時,劃分出不同的編譯層次:
- 第0層:程式解釋執行,解釋器不開啟性能監控功能,觸發 C1 編譯。
- 第1層:C1 編譯,将位元組碼編譯成本地代碼,進行簡單、可靠的優化,如有必要解釋器将開始性能監控。
- 第2層:C2 編譯,将位元組碼編譯成本地代碼,啟用一些編譯耗時較長的優化,甚至會根據性能監控資訊進行一些不可靠的激進優化。
- 使用 “-client” 強制虛拟機運作于 Client 模式。
- 使用 “-server” 強制虛拟機運作于 Server 模式。
- 使用 “-Xint” 強制虛拟機隻使用解釋器執行程式,編譯器不工作。
- 使用 “-Xcomp” 強制虛拟機隻使用編譯器執行程式,解釋器作為編譯器的“逃生門”。
- 使用 “-XX:+TieredCompilation” 開啟分層編譯。虛拟機 Server 模式下預設開啟。
三、熱點代碼探測
熱點代碼分為兩種:被多次調用的方法、被多次執行的循環體。多次是一個很泛的概念,那麼到底什麼時候才能把熱點代碼編譯成機器碼呢?HotSpot 虛拟機采用的是計數器的方式,它為每個方法(甚至是代碼塊)建立計數器,統計執行次數,如果執行次數達到一定的門檻值,就把這部分代碼編譯成機器碼。
探測“被多次調用的方法”的計數器稱為方法調用計數器(Invocation Counter),它統計的是一個方法調用的相對次數,即同一段時間内方法被調用的次數,當超過一定的時間限度,如果該方法的計數仍然不足以讓它送出給編譯器編譯,那麼該方法的計數就會被減少一半,這個過程稱為方法調用計數器熱度的衰減(Counter Decay),這段時間就被稱為此方法統計的半衰周期(Counter Half Life Time)。方法調用計數器的相關 JVM 參數如下:
- -XX:CompileThreshold 設定方法調用計數器的門檻值,Client 模式下預設是 1500 次, Server 模式下預設是 10000 次
- -XX:UseCounterDecay 設定 true/false 來開啟/關閉熱度衰減,預設開啟
- -XX:CounterHalfLifeTime 設定半衰期的周期,機關是秒(debug 虛拟機支援)
探測“被多次執行的循環體”的計數器稱為回邊計數器(Back Edge Counter),它統計的是該方法循環執行的絕對次數,沒有計數熱度衰減的過程。回邊計數器的相關 JVM 參數如下:
- -XX:OnStackReplacePercentage OSR比率,Client 模式下預設是 933,Server 模式下預設是 140;
- -XX:InterpreterProfilePercentage 解釋器監控比率,預設值是 33
- Client 模式的回邊計數器門檻值 = CompileThreshold * OnStackReplacePercentage/100,預設是 13995 次
- Server 模式的回邊計數器門檻值 = CompileThreshold * (OnStackReplacePercentage - InterpreterProfilePercentage)/100,預設是 10700 次
四、優化技術
HotSpot 的優化技術非常全面,實作起來也比較複雜,但是對于了解它們來說卻顯得沒那麼困難,我們将列舉幾項最有代表性的優化技術。
1. 方法内聯
方法内聯的重要性要優于其他優化措施,它的主要目的有兩個,一是去除方法調用的成本,二是為其他優化建立良好的基礎。
方法内聯的行為很簡單,就是把目标方法的代碼“複制”到發起調用的方法之中,避免發生真實的方法調用而已。
2. 公共子表達式消除
如果一個表達式 E 已經計算過了,并且從先前的計算到現在 E 中所有變量的值都沒有發生變化,那麼 E 的這次出現就成為了公共子表達式。對于這種表達式,沒有必要花時間再對它進行計算,隻需要直接用前面計算過的表達式結果代替 E 就可以了。我們來舉個例子來模拟下它的優化過程:
public static void main(String[] args) {
int a = 1;
int b = 1;
int c = 1;
int d = (c * b) * 12 + a + (a + b * c);
// 1. 提取公共子表達式
int E = c * b;
d = E * 12 + a + (a + E);
// 2. 代數化簡
d = E * 13 + a * 2;
}
3. 數組邊界檢查消除
當我們嘗試對數組越界通路的時候,Java 會向我們抛一個 java.lang.ArrayIndexOutOfBoundsException,這對軟體開發者來說是一件很好的事情,即使沒有專門編寫防禦代碼,也可以避免大部分的溢出攻擊,但是對虛拟機來說,意味着每一次的數組通路都帶有一次隐含的條件判定操作,即數組邊界檢查,那麼有沒有辦法消除這種檢查呢?
虛拟機一般是在即時編譯期間通過資料流分析來确定是否可以消除這種檢查,比如 foo[3] 的通路,隻有在編譯的時候确定 3 不會超過 foo.length - 1 的值,就可以判斷該次數組通路沒有越界,就可以把數組邊界檢查消除。
4. 逃逸分析
逃逸分析的基本行為就是分析對象動态作用域:當一個對象在方法被定義後,它可能被外部方法所引用,例如作為調用參數傳遞到其他方法中,稱為方法逃逸;甚至還有可能被外部線程通路到,譬如指派給類變量或可以在其他線程中通路的執行個體變量,稱為線程逃逸。
如果能證明一個對象不會逃逸到方法或者線程之外,則可以為這個變量進行一些高效的優化:
1) 棧上配置設定
如果确定一個對象不會逃逸出方法之外,假如能使用棧上配置設定這個對象,那大量的對象就會随着方法的結束而自動銷毀了,垃圾收集系統的壓力将會小很多。然而遺憾的是,目前的 HotSpot 虛拟機還沒有實作這項優化。
2)同步消除
如果确定一個對象不會被其他線程通路到,那麼這個變量就不存線上程間的争搶,對這個變量實施的同步措施也可以消除掉。
3)标量替換
标量:無法被進一步分解的資料,比如原始資料類型(int、long以及 reference 類型等)
聚合量:可以被持續分解的資料,典型的就是 Java 中對象,它們還可以被分解成成員變量等。
标量替換指的是如果把一個 Java 對象拆散分解,根據程式通路的情況,将其使用到的成員變量恢複到原始類型來通路。
如果能确定一個對象不會被外部通路,并且這個對象可以被拆散的話,那程式真正執行的時候就可能不建立這個對象,而改為直接建立它的若幹個被這個方法使用到的成員變量來代替。
- -XX:+DoEscapeAnalysis 手動開啟/關閉逃逸分析,預設開啟,C2 編譯器有效
- -XX:+PrintEscapeAnalysis 檢視逃逸分析的結果(debug 虛拟機支援)
- -XX:+EliminateAllocations 手動開啟/關閉标量替換,預設開啟
- -XX:+PrintEliminateAllocations 檢視标量替換情況(debug 虛拟機支援)
- -XX:+EliminateLocks 手動開啟/關閉同步消除,預設開啟
五、Graal 編譯器展望
Graal 編譯器是 JDK10 釋出的,由于這個編譯器使用 Java 編寫,代碼清晰,又繼承了許多來自 HotSpot 的服務端編譯器的高品質優化技術,是以無論是科技企業還是高校研究院,都願意在它上面研究和開發新技術。從 JDK10 起,Graal 編譯器可以替換服務端編譯器,成為 HotSpot 分層編譯中最頂層的即時編譯器。
對于 Graal 編譯器的推崇離不開 JDK9 釋出的 Java 虛拟機編譯器接口(JVM Compiler Interface,JVMCI),JVMCI 使得 Graal 可以從 HotSpot 的代碼中分離出來,使得 Graal 編譯器不至于像 C2 那樣跟 HotSpot 強耦合導緻越來越臃腫。
JVMCI 的出現,讓我們可以把一個在 HotSpot 虛拟機外部的、用 Java 語言實作的即時編譯器(例如 Graal)內建到 HotSpot 中。此外,又可以繞開 HotSpot 的即時編譯系統,讓編譯器直接為應用的類庫編譯出二進制機器碼,将編譯器當作一個提前編譯器去使用(如 Jaotc)。
Graal 編譯器仍處于實驗室階段,尚未商用,但未來有望代替或成為 HotSpot 的下一代技術基礎。