1. 程式人生 > >JVM 技術內幕——HotSpot VM 內的 JIT 編譯器

JVM 技術內幕——HotSpot VM 內的 JIT 編譯器

Java 的 “編譯期” 是一段 “不確定” 的操作過程,常見的編譯器包括:

編譯器 舉例 說明
前端編譯器 javac(java語言編寫的程式) 把*.java轉變為*.class的過程
JIT編譯器(即時編譯器) HotSpot VM的C1、C2編譯器(C++語言編寫的程式) 在執行期把位元組碼轉變為機器碼的過程
AOT編譯器(靜態提前編譯器) GCJ、Excelsior JET 直接把*.java檔案編譯成本地機器碼的過程

在主流的商用虛擬機器(Sun HotSpot、IBM J9)中,java程式最初是通過直譯器(Interpreter)進行解釋執行的,當虛擬機發現某個方法或程式碼塊的執行特別頻繁時,就會把這些程式碼認定為 “熱點程式碼”(Hot Spot Code)。為了提高熱點程式碼的執行效率,在執行時,JIT編譯器將會把這些程式碼編譯成與本地平臺相關的機器碼,並進行各種優化。

JIT編譯器編譯效能的好壞、程式碼優化程度的高低是衡量一款商用虛擬機器優秀與否的最關鍵的指標之一。

1.直譯器與編譯器

在主流的商用虛擬機器(Sun HotSpot、IBM J9)中,都採用直譯器與編譯器並存的架構,直譯器與編譯器各有優勢:

優勢
直譯器 直譯器可以首先發揮作用,省去編譯的時間,立即執行。
編譯器 在程式執行後,隨著時間推移,編譯器逐漸發揮作用,把越來越多的程式碼編譯成本地機器碼,獲取更高的執行效率。

在程式執行環境中記憶體資源限制較大(如部分嵌入式系統中),可以使用解釋執行節約記憶體,反之可以使用編譯執行來提升效率。同時,直譯器還可以作為編譯器激進優化時的一個 “逃生門”,讓編譯器根據概率選擇一些大多數時候都能提升執行速度的優化手段。當激進優化的假設不成立,如載入了新類後型別繼承結構出現變化、出現 “罕見陷阱” 時可以通過逆優化退回到解釋狀態繼續執行。直譯器與編譯器的互動如下:

----------------  -------即時編譯------->  ----------------
|    直譯器     |                          |      編譯器   |
|  Interpreter |                          | C1、C2編譯器   |
----------------  <-------逆優化----------  ----------------

2.HotSpot VM的JIT編譯器

HotSpot VM內建了兩個即時編譯器:

編譯器 說明 強制指定模式的引數
C1編譯器(Client Compiler) -client
C2編譯器(Server Compiler) -server

HotSpot VM預設採用直譯器與其中一個編譯器直接配合的方式工作,程式使用哪個,取決於虛擬機器執行的模式。

無論是C1還是C2,直譯器與編譯器搭配使用的方式在虛擬機器中稱為 “混合模式”,使用者可以使用引數 “-Xint” 強制虛擬機器運行於 “解釋模式”,這時編譯器完全不介入工作,全部程式碼都使用解釋執行。另外,也可以使用 “-Xcomp” 強制虛擬機器運行於 “編譯模式”,這時將優先採用編譯方式執行程式。

3.HotSpot VM的分層編譯策略

由於JIT編譯器編譯原生代碼需要佔用程式執行時間,要編譯出優化程度更高的程式碼,所費的時間更長。而且想要編譯出優化程度更高的程式碼,直譯器可能還要替編譯器收集效能資訊,這對直譯器執行的速度也有影響。為了在程度啟動響應速度與執行效率之間達到最佳平衡,HotSpot VM還會逐漸啟動分層編譯的策略,該策略在JDK1.7的Server模式中作為預設編譯策略被開啟。分層編譯根據編譯器編譯、優化的規模與耗時,劃分出不同的編譯層次,其中包括:

說明
第0層 程式解釋執行,直譯器不開啟效能監控功能(Profiling),可觸發第1層編譯。
第1層 也稱為C1編譯,將位元組碼編譯為原生代碼,進行簡單、可靠的優化,如有必要將加入效能監控的邏輯。
第2層 也稱為C2編譯,也是將位元組碼編譯為原生代碼,但是會啟動一些編譯耗時較長的優化,甚至會根據效能監控資訊進行一些不可靠的激進優化。

實施分層編譯後,C1、C2編譯器將會同時工作,許多程式碼都可能會被多次編譯,用C1編譯器獲取更高的編譯速度,用C2編譯器來獲取更好的編譯質量,在解釋執行的時候也無須再承擔收集效能監控資訊的任務。

4.編譯物件與觸發條件

熱點程式碼有兩類:

說明
被多次呼叫的方法 JIT編譯器會以整個方法作為編譯物件,這種編譯也是虛擬機器中標準的JIT編譯方法。
被多次執行的迴圈體 JIT編譯器會以整個方法作為編譯物件(而不是單獨的迴圈體)。這種編譯方式因為發生在方法執行過程中,因此形象地稱之為棧上替換(簡稱為OSR編譯,即方法棧幀還在棧上,方法就被替換了)。

判斷一段程式碼是否是熱點程式碼,是否需要觸發即時編譯,這樣的行為成為熱點檢測,目前主要的熱點檢測判定方式有兩種:

說明 優點 缺點
基於取樣的熱點檢測 週期性地檢查各個執行緒的棧頂,如果發現某個(或某些)方法經常出現在棧頂,那這個方法就是"熱點方法"。 簡單高效,還可以獲取方法呼叫關係(將呼叫堆疊展開即可) 很難精確確認一個方法的熱度,容易受到執行緒阻塞或別的外界因素的影響而擾亂熱點檢測
基於計數器的熱點檢測 為每個方法(甚至程式碼塊)建立計數器,統計方法的執行次數,如果執行次數超過一定的閾值就認為是"熱點方法" 統計結果相對更加精確與嚴謹 實現麻煩,不能直接獲取方法呼叫關係

在HotSpot VM中使用的是第二種,因此它為每個方法準備了兩類計數器:方法呼叫計數器、回邊計數器。在確定虛擬機器執行引數的前提下,這兩個計數器都有一個確定的閾值,當計數器超過閾值溢位了,就會觸發 JIT 編譯。

方法呼叫計數器的預設閾值在Client模式下是1500次,在Server模式下是10000次,這個閾值可以通過虛擬機器引數 -XX:CompileThreshold 來指定。