即時編譯回顧
HotSpot 虛擬機執行 Java 程序時,先通過解釋器對代碼解釋執行,發現某個方法或代碼塊執行比較頻繁后,對熱點代碼進行編譯,編譯后生成與本地平台相關的機器碼,再去執行機器碼獲得較高的運行效率。必要時,也會通過逆優化從即時編譯回到解釋執行,如編譯器遇到罕見陷阱的情況。

在 Java 虛擬機規范中,並未要求虛擬機必須實現即時編譯,但即時編譯在主流的虛擬機中都有實現,后文所有討論都基於 HotSpot 虛擬機。
即時編譯器
HotSpot 虛擬機中有兩個即時編譯器,分別為 Client Compiler 和 Server Compiler,簡稱為 C1 編譯器和 C2 編譯器。C1 編譯器占用內存小,進行局部優化,代碼執行效率比 C2 編譯器低,適用於客戶端應用; C2 編譯器占用內存大,進行全局優化,代碼執行效率比 C1 編譯器高,適用於服務端應用。在 JDK 1.7 以后的 HotSpot 虛擬機中,采用了被稱為分層編譯的策略,啟動時解釋執行提高啟動速度,然后 C1 編譯獲取較好的編譯速度,再 C2 編譯獲取較好的編譯質量。
默認情況下,虛擬機會自動選擇合適的編譯器與解釋器一起配合工作,用戶也可以在啟動參數里使用 “-client” 或 “-server” 來強制要求虛擬機使用某一種編譯器。編譯器與解釋器一起配合工作的模式,被稱為混合模式。用戶也可以在啟動參數里使用 “-Xint” 來要求虛擬機只使用解釋器,使用 “-Xcomp” 來要求虛擬機只使用編譯器。
被多次調用的方法和被多次執行的循環體,就是即時編譯器關注的熱點代碼。在 HotSpot 中,采用了基於計數器的熱點探測技術,為每個方法定義了兩個計數器:方法調用計數器、回邊計數器。
默認情況下,如果一段時間內方法調用計數器的值沒有超過虛擬機設置的閾值,則在垃圾回收時計數器會熱度衰減,數值減少 1/2,此時間范圍又被稱為半衰期。所以方法調用計數器中存儲的實際上並不是方法調用的絕對次數。用戶可以調整半衰期的值,甚至可以關閉熱度衰減。
回邊計數器則統計了循環體執行的絕對次數,它的閾值可以由方法調用計數器的閾值計算出來。如果回邊計數器發生溢出,也會把方法調用計數器調整為溢出。
調用方法時,如果兩個計數器之和超過了方法調用計數器的閾值,就會提交方法的編譯請求。循環體執行時,如果兩個計數器之和超過了回邊計數器的閾值,也是編譯代碼塊所在的方法,因為此時方法正在執行中,又被稱為棧上替換,即替換方法時方法的棧幀還在棧上。
默認情況下,編譯器在后台進行編譯,即調用熱點代碼發現需要編譯時,先以解釋方式繼續執行,向編譯器發送編譯請求,編譯結束后下次調用時才使用編譯的本地代碼。用戶也可以通過啟動參數來要求虛擬機禁止后台編譯,編譯結束前熱點代碼的執行處於阻塞狀態。
優化技術
HotSpot 虛擬機使用了很多種優化技術,這里只簡單介紹其中的幾種,完整的優化技術介紹可以參考官網內容。
公共子表達式消除:
如果一個表達式已經進行過計算,並且在下次用到之前依賴的變量沒有變化,即表達式的計算結果不會發生變化,則在下次使用這個表達式時直接使用計算的結果。
數組邊界檢查消除:
在 Java 中訪問數組時,會自動進行邊界檢查來防止數組下標越界。但是對於某些情況並不需要每次訪問都去檢查,如在一個循環中遍歷數組元素,如果虛擬機能夠確定下標不會發生越界並且優化確實能夠提高運行速度,則虛擬機會去除每次訪問的下標檢查。
方法內聯:
對於可以內聯的方法,直接復制到調用者代碼中,減少方法調用次數和性能消耗。
逃逸分析:
方法中定義的一個對象,如果會被其他方法訪問則稱為方法逃逸,如果會被其他線程訪問則稱為線程逃逸。對於不能逃逸的對象,HotSpot 虛擬機采用了棧上分配、同步消除、標量替換等方法進行優化。
每周 3 篇學習筆記或技術總結,面向有一定基礎的 Java 程序員,內容涉及 Java 進階、虛擬機、MySQL、NoSQL、分布式計算、開源框架等多個領域。關注作者或微信公眾號 backend-develop 第一時間獲取最新內容。
