一、概述 即時編譯器(Just In Time Compiler),也稱為 JIT 編譯器,它的主要工作是把熱點代碼編譯成與本地平臺相關的機器碼,併進行各種層次的優化,從而提高代碼執行的效率。 那麼什麼是熱點代碼呢?我們知道虛擬機通過解釋器(Interpreter)來執行位元組碼文件,當虛擬機發現某個 ...
一、概述
即時編譯器(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 編譯,將位元組碼編譯成本地代碼,啟用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。
tips:
- 使用 “-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 對象拆散分解,根據程式訪問的情況,將其使用到的成員變數恢復到原始類型來訪問。
如果能確定一個對象不會被外部訪問,並且這個對象可以被拆散的話,那程式真正執行的時候就可能不創建這個對象,而改為直接創建它的若幹個被這個方法使用到的成員變數來代替。
tips:
- -XX:+DoEscapeAnalysis 手動開啟/關閉逃逸分析,預設開啟,C2 編譯器有效
- -XX:+PrintEscapeAnalysis 查看逃逸分析的結果(debug 虛擬機支持)
- -XX:+EliminateAllocations 手動開啟/關閉標量替換,預設開啟
- -XX:+PrintEliminateAllocations 查看標量替換情況(debug 虛擬機支持)
- -XX:+EliminateLocks 手動開啟/關閉同步消除,預設開啟