什么是JIT:
JIT編譯器(just in time 即時編譯器),當虛擬機發現某個方法或代碼塊運行特別頻繁時,就會把這些代碼認定為(Hot Spot Code 熱點代碼,為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平台相關的機器碼,並進行各層次的優化,完成這項任務的正是JIT編譯器。
目前主要的熱點 判定方式有以下兩種:
- 基於采樣的熱點探測:
采用這種方法的虛擬機會周期性地檢查各個線程的棧頂,如果發現某些方法經常出現在棧頂,那這段方法代碼就是“熱點代碼”。這種探測方法的好處是實現簡單高效,還可以很容易地獲取方法調用關系,缺點是很難精確地確認一個方法的熱度,容易因為受到線程阻塞或別的外界因素的影響而擾亂熱點探測。 - 基於計數器的熱點探測:
采用這種方法的虛擬機會為每個方法,甚至是代碼塊建立計數器,統計方法的執行次數,如果執行次數超過一定的閥值,就認為它是“熱點方法”。這種統計方法實現復雜一些,需要為每個方法建立並維護計數器,而且不能直接獲取到方法的調用關系,但是它的統計結果相對更加精確嚴謹。
HotSpot虛擬機中使用的是第二種:基於計數器的熱點探測方法,因此它為每個方法准備了兩個計數器:方法調用計數器和回邊計數器。
• 方法調用計數器
方法調用計數器用來統計方法調用的次數,在默認設置下,方法調用計數器統計的並不是方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間內方法被調用的次數。
• 回邊計數器
用於統計一個方法中循環體代碼執行的次數(准確地說,應該是回邊的次數,因為並非所有的循環都是回邊),在字節碼中遇到控制流向后跳轉的指令就稱為“回邊”。
JIT編譯。觸發了JIT編譯后,在默認設置下,執行引擎並不會同步等待編譯請求完成,而是繼續進入解釋器按照解釋方式執行字節碼,直到提交的請求被編譯器編譯完成為止(編譯工作在后台線程中進行)。當編譯工作完成后,下一次調用該方法或代碼時,就會使用已編譯的版本。
方法調用計數器觸發即時編譯的流程(回邊計數器觸發即時編譯的過程類似)
JIT優化JVM的性能
通常JIT的有以下幾種手段來優化JVM的性能 - 針對特定CPU型號的編譯優化,JVM會利用不同CPU支持的SIMD指令集來編譯熱點代碼,提升性能。像intel支持的SSE2指令集在特定情況下可以提升近40倍的性能。
- 減少查表次數。比如調用Object.equals()方法,如果運行時發現一直是String對象的equals,編譯后的代碼可以直接調用String.equals方法,跳過查找該調用哪個方法的步驟。
- 逃逸分析。JAVA變量默認是分配在主存的堆上,但是如果方法中的變量未逃出使用的生命周期,不會被外部方法或者線程引用,可以考慮在棧上分配內存,減少GC壓力。另外逃逸分析可以實現鎖優化等提升性能方法。
- 寄存器分配,部分變量可以分配在寄存器中,相對於主存讀取,更大的提升讀取性能。
- 針對熱點代碼編譯好的機器碼進行緩存。代碼緩存具有固定的大小,並且一旦它被填滿,JVM 則不能再編譯更多的代碼。
- 方法內聯,也是JIT實現的非常有用的優化能力,同時是開發者能夠簡單參與JIT性能調優的地方。
方法內聯是什么。為什么它能夠提升性能
要搞清楚為什么方法內聯有用,首先要知道當一個函數被調用的時候發生了什么
- 首先會有個棧,存儲目前所有活躍的方法,以及它們的本地變量和參數
- 當一個新的方法被調用了,一個新的棧幀會被加到棧頂,分配的本地變量和參數會存儲在這個棧幀
- 跳到目標方法代碼執行
- 方法返回的時候,本地方法和參數會被銷毀,棧頂被移除
- 返回原來地址執行
因此,函數調用需要有一定的時間開銷和空間開銷,當一個方法體不大,但又頻繁被調用時,這個時間和空間開銷會相對變得很大,變得非常不划算,同時降低了程序的性能。
方法內聯就是把被調用方函數代碼"復制"到調用方函數中,來減少因函數調用開銷的技術。
被內聯前的代碼
private int add4(int x1, int x2, int x3, int x4) {
return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
return x1 + x2;
}
運行一段時間后,代碼被內聯翻譯成
private int add4(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}
JVM會自動的識別熱點方法,並對它們使用方法內聯優化。那么一段代碼需要執行多少次才會觸發JIT優化呢?通常這個值由-XX:CompileThreshold參數進行設置:
1、使用client編譯器時,默認為1500;
2、使用server編譯器時,默認為10000;
但是一個方法就算被JVM標注成為熱點方法,JVM仍然不一定會對它做方法內聯優化。其中有個比較常見的原因就是這個方法體太大了,分為兩種情況。
• 如果方法是經常執行的,默認情況下,方法大小小於325字節的都會進行內聯(可以通過** -XX:MaxFreqInlineSize=N來設置這個大小)
• 如果方法不是經常執行的,默認情況下,方法大小小於35字節才會進行內聯(可以通過 -XX:MaxInlineSize=N **來設置這個大小)
我們可以通過增加這個大小,以便更多的方法可以進行內聯;但是除非能夠顯著提升性能,否則不推薦修改這個參數。因為更大的方法體會導致代碼內存占用更多,更少的熱點方法會被緩存,最終的效果不一定好。
如果想要知道方法被內聯的情況,可以使用下面的JVM參數來配置
-XX:+PrintCompilation: Prints out when JIT compilation happens
-XX:+UnlockDiagnosticVMOptions: Is needed to use flags like -XX:+PrintInlining
-XX:+PrintInlining: Prints what methods were inlined
結論
熱點方法的內聯優化建議
- 更小的方法體
- 盡量使用final、private、static修飾符
- 使用+PrintInlining參數校驗效果