更多 Java 虛擬機(jī)方面的文章,請參見文集《Java 虛擬機(jī)》
前端編譯器 VS 后端編譯器
-
前端編譯器:
javac編譯,在程序運(yùn)行前,將 源文件 轉(zhuǎn)化為 字節(jié)碼 即 .class 文件- Java 程序最初只能通過解釋器解釋執(zhí)行,即 JVM 對字節(jié)碼逐條解釋執(zhí)行,因此執(zhí)行速度比較慢。
- 字節(jié)碼與平臺無關(guān)
-
后端編譯器:JIT 編譯,在程序運(yùn)行期間,將 字節(jié)碼 轉(zhuǎn)化為 機(jī)器碼
- 機(jī)器碼與平臺相關(guān)
JIT 即時編譯
當(dāng) JVM 發(fā)現(xiàn)某個方法或代碼塊執(zhí)行特別頻繁時,就將其認(rèn)定為 熱點(diǎn)代碼(Hot Spot Code)。在程序運(yùn)行期間,JVM 將這些熱點(diǎn)代碼編譯為與本地平臺相關(guān)的機(jī)器碼,并進(jìn)行各層次的優(yōu)化,從而提升熱點(diǎn)代碼的執(zhí)行效率。
基本流程如下:
如何檢測熱點(diǎn)代碼(Hot Spot Code):
- 基于 采樣 的熱點(diǎn)檢測:檢查各個線程的棧頂。
- 基于 計(jì)數(shù)器 的熱點(diǎn)檢測:HotSpot 虛擬機(jī)采用:
- 方法計(jì)數(shù)器:統(tǒng)計(jì)每個方法調(diào)用的次數(shù)
- 回邊計(jì)數(shù)器:統(tǒng)計(jì)每個方法中循環(huán)體代碼執(zhí)行的次數(shù)
即時編譯的設(shè)置及優(yōu)化
初級調(diào)優(yōu)
HotSpot 虛擬機(jī)內(nèi)置兩個 JIT 編譯器:
-
客戶模式 Client Compiler,即 C1 編譯
- 無采樣,立即 JIT 編譯,輕量優(yōu)化
- JIT 編譯的類較多,可能導(dǎo)致代碼緩存不夠用
- 速度較快,適用于短暫的應(yīng)用程序
-
服務(wù)器模式 Server Compiler,即 C2 編譯
- 采集 一萬次 調(diào)用樣本后深度編譯優(yōu)化
- JIT 編譯的類較少
- 啟動速度較慢,運(yùn)行起來后性能逐步提升
- 每次 GC,計(jì)數(shù)器衰減一半
- 可以設(shè)置
-XX:-UseCounterDelay來禁止衰減
Java 8 支持多層編譯,即程序啟動時使用 C1 編譯,樣本足夠后使用 C2 編譯:
- 禁止多層編譯:
-XX:-TieredCompilation - 啟用多層編譯:
-XX:+TieredCompilation
優(yōu)化代碼緩存
如果緩存過小,有些熱點(diǎn)代碼可能不會被 JIT 編譯。
C1 編譯的類較多,可能導(dǎo)致代碼緩存不夠用。
設(shè)置代碼緩存大小:-XX:ReservedCodeCacheSize = 32m
編譯閾值
即計(jì)數(shù)器的閾值,默認(rèn)為10000,即方法計(jì)數(shù)器和回邊計(jì)數(shù)器的總和達(dá)到了10000就觸發(fā) JIT 編譯。
設(shè)置編譯閾值:-XX:CompileThreshold = 10000
內(nèi)聯(lián) Inline
將方法的代碼復(fù)制到發(fā)起調(diào)用的方法里,以消除方法調(diào)用。
因?yàn)檎{(diào)用一個小方法可能比直接執(zhí)行該小方法對應(yīng)的代碼更耗時。
-
-XX:MaxInlineSize=35byte:能被內(nèi)聯(lián)的方法最大字節(jié)碼大小 -
-XX:FreqInlineSize=325byte:頻繁調(diào)用的方法能被內(nèi)聯(lián)的最大字節(jié)碼大小
如何讓方法更容易被內(nèi)聯(lián):拆分不常訪問的路徑。例如:
public void f() {
if(most case) {
...
}
else {
... // 將不常訪問的路徑的代碼拆分到函數(shù) g() 中
... // 以降低整體代碼的大小,使得 most case 中的代碼可以被內(nèi)聯(lián)
}
}
打印 JIT 編譯信息
java -XX:PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:PrintInline > a.out