javac生成字節(jié)碼,字節(jié)碼可以解釋執(zhí)行,也可以進(jìn)一步通過JIT編譯執(zhí)行,JIT把字節(jié)碼變?yōu)闄C(jī)器碼。
JVM采用解釋器interpreter與JIT編譯器compiler并存的架構(gòu):二者相互配合;當(dāng)程序需要迅速啟動和執(zhí)行的時候,解釋器首先發(fā)揮作用,省去編譯的時間,立即執(zhí)行。程序運(yùn)行后,編譯器逐漸把高頻調(diào)用代碼(熱點(diǎn)代碼)用JIT編譯為本地代碼(平臺相關(guān)機(jī)器碼),并進(jìn)行優(yōu)化,以獲取更高的執(zhí)行效率。JIT可以采用激進(jìn)的優(yōu)化方式,如果不成立可以回退到解釋器執(zhí)行。
HotSpot內(nèi)置了兩個JIT編譯器:Client/Server Compiler;默認(rèn)采用解釋器和其中一個編譯器直接配合的方式,虛擬機(jī)根據(jù)版本和宿主機(jī)器的硬件性能自動選擇運(yùn)行模式,進(jìn)而根據(jù)運(yùn)行模式?jīng)Q定使用哪個編譯器。用戶也可以用-client/server參數(shù)強(qiáng)制設(shè)定虛擬機(jī)的運(yùn)行模式。另外,可以通過參數(shù)指定混合/解釋/編譯模式。-Xint:強(qiáng)制虛擬機(jī)運(yùn)行于解釋模式;-Xcomp:強(qiáng)制虛擬機(jī)運(yùn)行于編譯模式
一、JIT分層編譯
client/server編譯器同時工作,C1/client編譯獲取更高的編譯速度,C2/server獲取更好的編譯質(zhì)量。解決的問題:JIT編譯為本地代碼進(jìn)行優(yōu)化都需要占用運(yùn)行時時間,程序啟動響應(yīng)時間和運(yùn)行效率需要平衡
第0層:解釋執(zhí)行,解釋器不開啟性能監(jiān)控,可觸發(fā)第1層;第1層:C1編譯,將字節(jié)碼編譯為本地代碼,簡單可靠優(yōu)化,可加入性能監(jiān)控邏輯;第2層:C2編譯,啟動耗時長的優(yōu)化,根據(jù)性能監(jiān)控信息進(jìn)行激進(jìn)優(yōu)化。
二、JIT編譯對象與觸發(fā)條件 - 熱點(diǎn)代碼
熱點(diǎn)代碼包含多次調(diào)用的方法和多次執(zhí)行的循環(huán)體OSR(棧上替換),方法幀還在棧上就被替換了。
熱點(diǎn)探測用于判斷代碼是不是熱點(diǎn)代碼。
1. 基于采樣的熱點(diǎn)探測:虛擬機(jī)周期性的檢查各個線程的棧頂,如果發(fā)現(xiàn)某個方法經(jīng)常出現(xiàn)在棧頂,那就是熱點(diǎn)方法。
2. 基于計數(shù)器的熱點(diǎn)探測:為每個方法建立計數(shù)器,當(dāng)調(diào)用次數(shù)超過閾值就認(rèn)為是熱點(diǎn)代碼;HotSpot為每個方法設(shè)備兩類計數(shù)器:
1)方法調(diào)用計數(shù)器:client模式默認(rèn)閾值1500;server模式10000;可通過CompileThreshold參數(shù)配置。方法被調(diào)用時,先檢查該方法有沒有JIT編譯過的版本,有則執(zhí)行本地代碼;沒有則將方法的調(diào)用計數(shù)器加1,判斷閾值,超過閾值則向JIT提交編譯請求。執(zhí)行引擎不會同步等待編譯請求完成,而是繼續(xù)進(jìn)入解釋器解釋執(zhí)行;JIT編譯完成后,方法的調(diào)用入口地址自動改寫成新的。BackgroundCompilation用于配置禁止后臺編譯,執(zhí)行線程提交編譯請求后保持等待。
熱度衰減:采用半衰周期內(nèi)調(diào)用次數(shù),而非絕對次數(shù);超過半衰周期后調(diào)用次數(shù)仍未達(dá)到閾值,調(diào)用計數(shù)器減半。UseCounterDecay配置關(guān)閉熱度衰減,讓計數(shù)器統(tǒng)計絕對值;CounterHalfLifeTime配置半衰周期;
2)回邊計數(shù)器:統(tǒng)計方法中循環(huán)體的執(zhí)行次數(shù);回邊表示字節(jié)碼中遇到的控制流向后跳轉(zhuǎn)的指令。閾值:根據(jù)方法調(diào)用計數(shù)器閾值/OSR比率/解釋器監(jiān)控比率(server模式)計算得出;統(tǒng)計絕對次數(shù)。OnStackReplacePercentage配置OSR比率;InterpreterProfilePercentage配置解釋器監(jiān)控比率
三、JIT編譯過程
1. Client編譯
簡單快速的三段式編譯器,關(guān)注點(diǎn)在局部優(yōu)化,放棄耗時長的全局優(yōu)化。
1. 平臺獨(dú)立的前端把字節(jié)碼構(gòu)造為高級中間代碼HIR,基礎(chǔ)優(yōu)化如方法內(nèi)聯(lián)/常量傳播;
2. 平臺相關(guān)的后端把HIR轉(zhuǎn)化為低級中間代碼LIR,優(yōu)化如空值檢查消除/范圍檢查消除;
3. 平臺相關(guān)的后端把LIR轉(zhuǎn)化為機(jī)器代碼,使用線性掃描算法分配寄存器,窺孔優(yōu)化;
2. Server編譯
高級優(yōu)化如無用代碼消除,循環(huán)展開,循環(huán)表達(dá)式外提,消除公共子表達(dá)式,常量傳播,基本塊重排序,范圍檢查消除,空值檢查消除。根據(jù)解釋器/client編譯器提供的性能監(jiān)控信息,進(jìn)行激進(jìn)優(yōu)化,如守護(hù)內(nèi)聯(lián),分支頻率預(yù)測。
四、編譯優(yōu)化技術(shù)
程序員的共識:編譯執(zhí)行比解釋執(zhí)行快,因為虛擬機(jī)幾乎所有優(yōu)化都在JIT的設(shè)計中。
1)方法內(nèi)聯(lián):重要性高于其他優(yōu)化措施,通常放在優(yōu)化序列靠前的位置;內(nèi)聯(lián)可以去掉方法調(diào)用的成本(如建立幀),同時方法內(nèi)聯(lián)膨脹便于在更大范圍上采取后續(xù)優(yōu)化手段。難點(diǎn)在于java方法大多是虛方法,運(yùn)行時才知道方法接受者,屬于激進(jìn)優(yōu)化;內(nèi)聯(lián)緩存:在未發(fā)生方法調(diào)用前,內(nèi)聯(lián)緩存為空,第一次調(diào)用后,記錄方法接受者的版本信息,如果后續(xù)方法接受者一致,那就使用該內(nèi)聯(lián)。
2)冗余訪問消除:y=b.value; z=b.value,替換為z=y;
3)復(fù)寫傳播:y=b.value; z=b.value;z變量沒有存在的必要,可以用y變量替換
4)公共子表達(dá)式消除:如果一個表達(dá)式E已經(jīng)計算過了,并且所有變量值沒有變化,那么E就是公共表達(dá)式,直接用之前的計算結(jié)果替換E。根據(jù)應(yīng)用范圍分為局部/全局。
5)數(shù)組邊界檢查消除:java數(shù)組訪問array[i]有自動的上下界限訪問檢查,這種檢查降低了編碼出錯的概率,但也有開銷,優(yōu)化方式比如在循環(huán)中進(jìn)行數(shù)組訪問,只要循環(huán)的終點(diǎn)最大值沒有越界,那么前面數(shù)組節(jié)點(diǎn)的訪問可以不做檢查。
6)逃逸分析:分析對象動態(tài)作用域,當(dāng)對象在方法中被定義,他可能通過參數(shù)傳遞方式被其他方法引用,稱為方法逃逸,甚至可能通過賦值給類變量而被其他線程引用,稱為線程逃逸。如果能證明對象不會逃逸,那么可以進(jìn)行高效優(yōu)化。
7)棧上分配內(nèi)存:對象放在棧幀而不是堆,對象內(nèi)存隨著出棧銷毀,減輕GC壓力
8)同步消除:如果對象不被線程共享則消除耗時的同步措施
9)標(biāo)量替換:標(biāo)量指無法再分解的數(shù)據(jù),如java原始類型;與之相對的是聚合量,如java對象。標(biāo)量替換指:把java對象拆散,根據(jù)程序訪問情況,將用到的成員變量作為原始類型訪問。可以省去創(chuàng)建對象的開銷,把對象的成員變量存在棧上,而棧上的數(shù)據(jù)通常會被存儲到寄存器。
五、Java編譯器 vs c/c++編譯器
總體而言,java用運(yùn)行效率的劣勢換取開發(fā)效率的優(yōu)勢。JIT占用運(yùn)行時時間,限制了大規(guī)模優(yōu)化技術(shù)的引入;Java類型檢查提高安全性的同時,也是耗時的操作;Java中大量使用虛方法,導(dǎo)致內(nèi)聯(lián)難度加大;Java由虛擬機(jī)而不是程序員負(fù)責(zé)GC,效率上也有犧牲。
JIT編譯器配置:
PrintCompilation:打印被即時編譯的方法名稱,帶%的是OSR觸發(fā);
PrintInlining:輸出方法內(nèi)聯(lián)信息
PrintAssembly:打印生成字節(jié)碼的反匯編結(jié)果(匯編代碼)
PrintOptoAssembly / PrintLIR:輸出中間代碼