“代碼跑著跑著,就變快了?”——揭秘Java性能幕后引擎:即時編譯器
HotSpot虛擬機內部集成了兩個即時編譯器,分別被稱為C1編譯器(Client Compiler/ Quick Complier)和C2編譯器(Server Compiler)。自Java 9起,-server模式(即啟用C2編譯器或分層編譯)是默認選項,-client選項通常會被忽略。
C1編譯器的啟動速度較快,主要關注局部的、簡單且可靠的優化策略,例如方法內聯、常量傳播、死代碼消除、冗余消除等。相比之下,C2編譯器則專注于全局優化,這些優化通常需要更長的編譯時間,甚至會根據性能監控(profiling)數據進行一些激進但不一定可靠的優化,例如更復雜的內聯決策、逃逸分析、循環優化、向量化等。C2編譯器的性能通常比C1編譯器高出30%以上,因此更適合長時間運行的后臺程序。
從Java 7開始引入,并在Java 8中成為默認策略(當C2可用時),分層編譯結合了C1的快速啟動和C2的高峰值性能。它將編譯過程劃分為5個層次。
1)第0層:解釋執行收集性能監控數據,主要是方法調用計數器和循環回邊計數器。
2)第1層:C1編譯器(Simple C1)不進行Profiling,快速編譯為本地代碼。
3)第2層:C1編譯器(Limited Profile C1)進行少量的Profiling(調用次數、循環次數)。
4)第3層:C1編譯器(Full Profile C1)進行全面的Profiling,收集包括分支頻率、類型信息等更詳細的數據,為C2做準備。
5)第4層:C2編譯器利用C1收集到的詳盡Profiling數據,進行最大程度的優化編譯。
性能監控是在程序執行過程中收集反映代碼執行狀態的數據,如方法調用頻率、循環執行頻率、分支跳轉信息、類型剖面等。這些數據是即時編譯器(尤其是C2)做出明智優化決策的依據。性能監控的精度越高,其帶來的額外性能開銷就越大。最基本的是方法調用計數器和循環回邊計數器,用于識別熱點代碼并觸發即時編譯。編譯閾值是動態的,并且受分層編譯策略的影響,但傳統的Client模式下默認閾值約為1500次調用,Server模式下約為10000次調用(這些具體數字可能隨JDK版本和模式變化)。

方法調用計數器
方法調用計數器(Invocation counter),顧名思義,這個計數器就是用于統計方法被調用的次數。需注意該計數器統計的非絕對次數,而是衡量一個相對的執行頻率。當超過一定的時間限度,如果方法的調用次數仍不足以觸發即時編譯,那這個方法的調用計數會被減少一半,這個過程稱為熱度的衰減 (Counter decay),而這段時間就稱為此方法統計的半衰周期 (Counter half life time)。
@RequestMapping(value = "/input")
public CommonResponse input(@RequestBody InputRequest request) {
// 如果 input 方法本身成為熱點,它會被JIT編譯。
// JIT編譯器可能會決定將 doSomething 方法內聯到 input 方法中,
// 如果 doSomething 方法符合內聯條件(如方法體小、調用頻繁等)。
return CommonResponse.ok(doSomething(request));
}
public void doSomething(InputRequest request) {
// 如果 doSomething 方法自身被頻繁調用(無論是直接調用還是通過 input 間接調用),
// 并且達到了編譯閾值,它也會被JIT編譯成本地機器碼。
// ... 復雜的業務邏輯 ...
}
循環回邊計數器
循環回邊計數器(Loop backEdge counter)會對程序中的循環進行計數。每當程序執行一次循環的回邊(即從循環的末尾跳回到循環的開始),循環回邊計數器的值就會增加。
void loop() {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
}
上面這段代碼經過編譯生成下面的字節碼:
public void loop();
Code:
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_2
5: bipush 10
7: if_icmpge 20
10: iload_1
11: iload_2
12: iadd
13: istore_1
14: iinc 2, 1
17: goto 4
20: return
在上述字節碼中,循環回邊計數器被存儲在第7行的if_icmpge指令中。if_icmpge指令用于接收兩個操作數用于比較計算,以決定循環體跳轉的位置。在解釋執行時,每當運行一次該指令,該方法的循環回邊計數器加1。
循環回邊計數器觸發的優化編譯技術叫作棧上替換 (On stack replacement,OSR) 。假設有一個方法只被調用一次,但卻包含超過一萬次以上循環迭代次數,這個循環方法無法以方法調用計數來統計。而棧上替換技術解決了這個問題。當編譯器檢測到一個循環已經迭代次數達到閾值時,動態地將這個循環(以及包含它的方法的一部分)編譯成本地機器碼,并讓當前正在執行的線程“切換”到新編譯的代碼上繼續執行循環,而無需等待方法調用結束。
void largeLoop() {
// 假設此方法只被調用一次
long sum = 0;
// 1. 循環回邊計數器通過迭代統計,即使方法調用次數少,此循環也會變熱。
// 2. 當達到OSR閾值,JIT會將循環部分編譯成本地機器碼。
// 3. 正在執行的線程會從解釋執行(或C1代碼)的循環“棧上替換”到新編譯的C2代碼。
for (int i = 0; i < 100000000; i++) { // 非常大的循環次數
sum += i;
// ... 其他操作 ...
}
System.out.println(sum);
}
未完待續
很高興與你相遇!如果你喜歡本文內容,記得關注哦!
本文來自博客園,作者:poemyang,轉載請注明原文鏈接:http://www.rzrgm.cn/poemyang/p/19022518
浙公網安備 33010602011771號