在部分的商用虛擬機中,Java 程序最初是經過解釋器( Interpreter )進行解釋執行的,當虛擬機發現某個方法或代碼塊的運行特別頻繁的時候,就會把這些代碼認定爲「熱點代碼」。爲了提升熱點代碼的執行效率,在運行時,即時編譯器(Just In Time Compiler )會把這些代碼編譯成與本地平臺相關的機器碼,並進行各類層次的優化。前端
解釋器和編譯器各有各的優勢:算法
解釋器優勢:當程序須要迅速啓動的時候,解釋器能夠首先發揮做用,省去了編譯的時間,當即執行。解釋執行佔用更小的內存空間。同時,當編譯器進行的激進優化失敗的時候,還能夠進行逆優化來恢復到解釋執行的狀態。express
編譯器優勢:在程序運行時,隨着時間的推移,編譯器逐漸發揮做用,把愈來愈多的代碼編譯成本地代碼以後,能夠得到更高的執行效率。後端
所以,整個虛擬機執行架構中,解釋器與編譯器常常配合工做,以下圖所示。架構
HotSpot中內置了兩個即時編譯器,分別稱爲 Client Compiler和 Server Compiler ,或者簡稱爲 C1 編譯器和 C2 編譯器。目前的 HotSpot 編譯器默認的是解釋器和其中一個即時編譯器配合的方式工做,具體是哪個編譯器,取決於虛擬機運行的模式,HotSpot 虛擬機會根據自身版本與計算機的硬件性能自動選擇運行模式,用戶也可使用 -client 和 -server 參數強制指定虛擬機運行在 Client 模式或者 Server 模式。這種配合使用的方式稱爲「混合模式」(Mixed Mode),用戶可使用參數 -Xint 強制虛擬機運行於 「解釋模式」(Interpreted Mode),這時候編譯器徹底不介入工做。另外,使用 -Xcomp 強制虛擬機運行於 「編譯模式」(Compiled Mode),這時候將優先採用編譯方式執行,可是解釋器仍然要在編譯沒法進行的狀況下接入執行過程。經過虛擬機 -version 命令能夠查看當前默認的運行模式。oop
在運行過程當中會被即時編譯的「熱點代碼」有兩類,即:性能
- 被屢次調用的方法
- 被屢次執行的循環體
對於第一種,編譯器會將整個方法做爲編譯對象,這也是標準的JIT 編譯方式。對於第二種是由循環體出發的,可是編譯器依然會以整個方法做爲編譯對象,由於發生在方法執行過程當中,稱爲棧上替換。
判斷一段代碼是不是熱點代碼,是否是須要出發即時編譯,這樣的行爲稱爲熱點探測(Hot Spot Detection),探測算法有兩種,分別爲。優化
- 基於採樣的熱點探測(Sample Based Hot Spot Detection):虛擬機會週期的對各個線程棧頂進行檢查,若是某些方法常常出如今棧頂,這個方法就是「熱點方法」。好處是實現簡單、高效,很容易獲取方法調用關係。缺點是很難確認方法的reduce,容易受到線程阻塞或其餘外因擾亂。
- 基於計數器的熱點探測(Counter Based Hot Spot Detection):爲每一個方法(甚至是代碼塊)創建計數器,執行次數超過閾值就認爲是「熱點方法」。優勢是統計結果精確嚴謹。缺點是實現麻煩,不能直接獲取方法的調用關係。
HotSpot 使用的是第二種-基於技術其的熱點探測,而且有兩類計數器:方法調用計數器(Invocation Counter )和回邊計數器(Back Edge Counter )。線程
這兩個計數器都有一個肯定的閾值,超事後便會觸發 JIT 編譯。server
首先是方法調用計數器。Client 模式下默認閾值是 1500 次,在 Server 模式下是 10000次,這個閾值能夠經過 -XX:CompileThreadhold 來人爲設定。若是不作任何設置,方法調用計數器統計的並非方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間以內的方法被調用的次數。當超過必定的時間限度,若是方法的調用次數仍然不足以讓它提交給即時編譯器編譯,那麼這個方法的調用計數器就會被減小一半,這個過程稱爲方法調用計數器熱度的衰減(Counter Decay),而這段時間就成爲此方法的統計的半衰週期( Counter Half Life Time)。進行熱度衰減的動做是在虛擬機進行垃圾收集時順便進行的,可使用虛擬機參數 -XX:CounterHalfLifeTime 參數設置半衰週期的時間,單位是秒。整個 JIT 編譯的交互過程以下圖。
第二個回邊計數器,做用是統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向後跳轉的指令稱爲「回邊」( Back Edge )。顯然,創建回邊計數器統計的目的就是爲了觸發 OSR 編譯。關於這個計數器的閾值, HotSpot 提供了 -XX:BackEdgeThreshold 供用戶設置,可是當前的虛擬機實際上使用了 -XX:OnStackReplacePercentage 來簡介調整閾值,計算公式以下:
在 Client 模式下, 公式爲 方法調用計數器閾值(CompileThreshold)X OSR 比率(OnStackReplacePercentage)/ 100 。其中 OSR 比率默認爲 933,那麼,回邊計數器的閾值爲 13995。
在 Server 模式下,公式爲 方法調用計數器閾值(Compile Threashold)X (OSR (OnStackReplacePercentage)- 解釋器監控比率 (InterpreterProfilePercent))/100
其中 onStackReplacePercentage 默認值爲 140,InterpreterProfilePercentage 默認值爲 33,若是都取默認值,那麼 Server 模式虛擬機回邊計數器閾值爲 10700 。
執行過程,以下圖。
默認狀況下,不管是方法調用產生的即時編譯請求,仍是 OSR 請求,虛擬機在代碼編譯器還未完成以前,都仍然將按照解釋方式繼續執行,而編譯動做則在後臺的編譯線程中進行,用戶能夠經過參數 -XX:-BackgroundCompilation 來禁止後臺編譯,這樣,一旦達到 JIT 的編譯條件,執行線程向虛擬機提交便已請求以後便會一直等待,直到編譯過程完成後再開始執行編譯器輸出的本地代碼。
對於 Client 模式而言
它是一個簡單快速的三段式編譯器,主要關注點在於局部的優化,放棄了許多耗時較長的全局優化手段。
- 第一階段,一個平臺獨立的前端將字節碼構形成一種高級中間代碼表示(High-Level Intermediate Representaion , HIR)。在此以前,編譯器會在字節碼上完成一部分基礎優化,如 方法內聯,常量傳播等優化。
- 第二階段,一個平臺相關的後端從 HIR 中產生低級中間代碼表示(Low-Level Intermediate Representation ,LIR),而在此以前會在 HIR 上完成另一些優化,如空值檢查消除,範圍檢查消除等,讓HIR 更爲高效。
- 第三階段,在平臺相關的後端使用線性掃描算法(Linear Scan Register Allocation)在 LIR 上分配寄存器,作窺孔(Peephole)優化,而後產生機器碼。
Client Compiler 的大體執行過程以下圖所示:
對於 Server Compiler 模式而言
它是專門面向服務端的典型應用,併爲服務端的性能配置特別調整過的編譯器,也是一個充分優化過的高級編譯器,幾乎能達到 GNU C++ 編譯器使用-O2 參數時的優化強度,它會執行全部的經典的優化動做,如 無用代碼消除(Dead Code Elimination)、循環展開(Loop Unrolling)、循環表達式外提(Loop Expression Hoisting)、消除公共子表達式(Common Subexpression Elimination)、常量傳播(Constant Propagation)、基本塊衝排序(Basic Block Reordering)等,還會實施一些與 Java 語言特性密切相關的優化技術,如範圍檢查消除(Range Check Elimination)、空值檢查消除(Null Check Elimination ,不過並不是全部的空值檢查消除都是依賴編譯器優化的,有一些是在代碼運行過程當中自動優化 了)等。另外,還可能根據解釋器或Client Compiler 提供的性能監控信息,進行一些不穩定的激進優化,如 守護內聯(Guarded Inlining)、分支頻率預測(Branch Frequency Prediction)等。
Server Compiler 編譯器能夠充分利用某些處理器架構,如(RISC)上的大寄存器集合。從即時編譯的角度來看, Server Compiler 無疑是比較緩慢的,但它的便以速度仍遠遠超過傳統的靜態優化編譯器,並且它相對於 Client Compiler編譯輸出的代碼質量有所提升,能夠減小本地代碼的執行時間,從而抵消了額外的編譯時間開銷,因此也有不少非服務端的應用選擇使用 Server 模式的虛擬機運行。