JVM基礎回顧
Java 作為一門高級程序語言,由于它自身的語言特性,它并非直接在硬件上運行,而是通過編譯器(前端編譯器)將 Java 程序轉換成該虛擬機所能識別的指令序列,也就是字節碼,然后運行在虛擬機之上的;JVM的存在主要
- 提供了可移植性,一旦 Java 代碼被編譯為 Java 字節碼,便可以在不同平臺上的 Java 虛擬機實現上運行,選擇在虛擬機上實現就可以避免在硬件上實現的高成本
- 提供了一個代碼托管的環境,代替我們處理部分冗長而且容易出錯的事務,例如內存管理。以及在這個運行時的環境里可以加入垃圾回收,類型檢查,安全權限等功能,使開發人員免于書寫無關業務邏輯的代碼,提升開發效率
那么我們知道從硬件視角來看,Java 字節碼無法直接執行。因此,Java 虛擬機需要將字節碼翻譯成機器碼。這里也是java與一些靜態編譯語言的不同。
在 HotSpot 里面,上述翻譯過程有兩種形式:第一種是解釋執行,即逐條將字節碼翻譯成機器碼并執行;第二種是即時編譯(Just-In-Time compilation,JIT),即將一個方法中包含的所有字節碼編譯成機器碼后再執行。
前者的優勢在于啟動速度快,無需等待編譯,無額外內存占用;而后者的優勢在于實際運行速度更快,程序啟動后,編譯器逐漸發揮作用,把越來越多的熱點代碼優化編譯保存成成本地代碼,可以減少解釋器的中間消耗,提高執行效率。
HotSpot 默認采用混合模式,綜合了解釋執行和即時編譯兩者的優點。它會先解釋執行字節碼,而后將其中反復執行的熱點代碼,以方法為單位進行即時編譯。
Hot Spot即時編譯器
為了滿足不同用戶場景的需要,HotSpot 內置了多個即時編譯器:C1、C2 和 Graal(Java 10 引入的實驗性即時編譯器)。
C1 又叫做 Client 編譯器,面向的是對啟動性能有要求的客戶端程序,采用的優化手段相對簡單,因此編譯時間較短。
C2 又叫做 Server 編譯器,面向的是對峰值性能有要求的服務器端程序,采用的優化手段相對復雜,因此編譯時間較長,但同時生成代碼的執行效率較高。
在 Java 7 以前,我們需要根據程序的特性選擇對應的即時編譯器。啟動性能有要求的程序,我們采用編譯效率較快的 C1。對于執行時間較長的,或者對峰值性能有要求的程序,我們采用生成代碼執行效率較快的 C2。
Java 8 默認使用分層編譯,可以動態的選擇使用C1和C2編譯器。方法代碼會先被解釋器解釋執行,然后熱點方法首先會被 C1 編譯,而后熱點方法中的熱點會進一步被 C2 編譯。
分層編譯的引入在于讓即時編譯更具備靈性,針對不同代碼的實際情況選取最佳的編譯路徑,也是在程序啟動速度、編譯時間與運行效率之間達到最佳平衡。
那么具體分層編譯的運行機制是什么呢?(http://cr.openjdk.java.net/~iveresov/tiered/Tiered.pdf)
- 解釋執行;(帶 profiling , profiling 是指在程序執行過程中,收集能夠反映程序執行狀態的數據)
- 執行不帶 profiling 的 C1 代碼;
- 執行僅帶方法調用次數以及循環回邊執行次數 profiling 的 C1 代碼;
- 執行帶所有 profiling 的 C1 代碼;
- 執行 C2 代碼。
通常情況下,C2 代碼的執行效率要比 C1 代碼的高出 30% 以上。
對于 C1 代碼的三種狀態,按執行效率從高至低則是 1 層 > 2 層 > 3 層
即時編譯的觸發
Java 虛擬機是根據方法的調用次數以及循環回邊的執行次數來觸發即時編譯的。上邊提到,Java 虛擬機在分層編譯 0 層、2 層和 3 層執行狀態時進行 profiling,其中就包含方法的調用次數和循環回邊的執行次數。
循環回邊:字節碼中可以簡單理解為往回跳轉的指令;
具體來說,在不啟用分層編譯的情況下,當方法的調用次數和循環回邊的次數的和,超過由參數 -XX:CompileThreshold 指定的閾值時(使用 C1 時,該值為 1500;使用 C2 時,該值為 10000),便會觸發即時編譯。當啟用分層編譯時,Java 虛擬機將不再采用由參數 -XX:CompileThreshold 指定的閾值(該參數失效),而是使用另一套閾值系統。在這套系統中,閾值的大小是動態調整的。所謂的動態調整其實并不復雜:在比較閾值時,Java 虛擬機會將閾值與某個系數 s 相乘。該系數與當前待編譯的方法數目成正相關,與編譯線程的數目成負相關。
|
通常情況下,方法會首先被解釋執行,然后被 3 層的 C1 編譯,最后被 4 層的 C2 編譯。
http://cr.openjdk.java.net/~iveresov/tiered/Tiered.pdf
即時編譯的優化
基于分支的優化
假設應用程序調用該方法時,所傳入的 boolean 值皆為 true。那么針對兩次判斷,false跳轉的次數都為 0。C2 可以根據這兩個分支 profile 作出假設,在接下來的執行過程中,這兩個條件跳轉指令仍舊不會發生跳轉。基于這個假設,C2 便不再編譯這兩個條件跳轉語句所對應的 false 分支了。
那么優化后的C代碼將直接返回0
;
根據條件跳轉指令的分支 profile,即時編譯器可以將從未執行過的分支剪掉,以避免編譯這些很有可能不會用到的代碼,從而節省編譯時間以及部署代碼所要消耗的內存空間。分支 profile 出現僅跳轉或者僅不跳轉的情況并不多見。即時編譯器對分支 profile 的利用也不僅限于“剪枝”。它還會根據分支 profile,計算每一條程序執行路徑的概率,以便某些編譯器優化優先處理概率較高的路徑。當假設失敗的情況下,Java 虛擬機給出的解決方案便是去優化,即從執行即時編譯生成的機器碼切換回解釋執行
基于類型的優化
方法內聯
公共子表達式消除
數組邊界檢查消除
逃逸分析
總結:
1.即時編譯-將Java字節碼編譯成可優化可復用的機器碼,運行在底層硬件之上,這么做是為了提高代碼的執行效率,提高性能峰值,其觸發點是熱點代碼,熱點代碼是通過方法的調用次數或者回邊循環的次數來篩選的
2.分層編譯的引入是為了讓即時編譯更具備靈性,使得虛擬機可以根據實際運行情況以及相應的算法動態選擇執行代碼的編譯路徑,通常情況下,熱點方法會先被解釋執行,然后被C1編譯,再被C2編譯
? ?分層編譯是一種折衷的方式,既能夠滿足部分不熱的代碼能夠在短時間內執行完成,也能滿足很熱的代碼能夠擁有最好的優化、執行效率.
資料來源:
1.《深入拆解虛擬機》 鄭雨迪