JIT:Java程序最初是由解釋器解釋執(zhí)行的驯杜,當虛擬機發(fā)現(xiàn)某個方法或代碼塊的運行特別頻繁時析砸,就會把這些“熱點代碼”編譯成機器碼,提高運行效率燃异。
1. 為什么要使用解釋器和編譯器并存坚嗜?
- 當需要程序快速啟動和執(zhí)行的時候夯膀,可以使用解釋器,省去編譯時間苍蔬,立即執(zhí)行诱建。隨著時間推移,編譯器逐漸發(fā)揮作用碟绑,編譯為機器碼俺猿,可以提高效率。
- 內存限制較大時格仲,解釋器執(zhí)行可以節(jié)約內存押袍,反之編譯器執(zhí)行可以提高效率。
- 解釋器可以作為編譯器激進優(yōu)化的逃生門凯肋。
2. HotSpot的兩個不同的即時編譯器
- Client Compiler:C1編譯器
- Server Complier:C2編譯器
- interpreted mode:純解釋模式
- compiled mode:純編譯模式谊惭,無法編譯是解釋器還是會介入的
- mixed mode:解釋器和編譯器搭配
要想編譯出優(yōu)化程度高的代碼,需要時間成本侮东,所以虛擬機為了權衡圈盔,采用了分層編譯。采用分層編譯后悄雅,C1和C2同時工作习贫,C1獲得更高的編譯速度厉碟,C2獲得更好的編譯質量。
- 第0層:程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控氯葬,可觸發(fā)第1層編譯。
- 第1層:C1編譯,簡單、可靠的優(yōu)化我碟,必要時加入性能監(jiān)控。
- 第2層:啟動一些編譯耗時較長的優(yōu)化姚建,甚至根據(jù)性能監(jiān)控信息進行不可靠的激進優(yōu)化。
3. 編譯對象和觸發(fā)條件
熱點代碼:
- 被多次調用的方法:整個方法為編譯對象吱殉,虛擬機標準的JIT編譯方式掸冤。
- 被多次執(zhí)行的循環(huán)體。雖然循環(huán)在方法體內友雳,但還是以整個方法為編譯對象稿湿,這種編譯方式為棧上替換,因為發(fā)生在方法執(zhí)行過程中押赊,方法幀還在棧上饺藤。
熱點探測:
- 基于采樣的熱點探測:虛擬機周期性檢查各個線程的棧頂,如果某個方法經(jīng)常出現(xiàn)在棧頂流礁,說明是熱點涕俗。優(yōu)點:簡單高效、容易獲取方法調用關系神帅。缺點:很難精確確認一個方法的熱度再姑,容易受到線程阻塞或別的干擾。
- 基于計數(shù)器的熱點探測:虛擬機為每個方法甚至代碼塊建立計數(shù)器找御,統(tǒng)計方法執(zhí)行次數(shù)元镀。優(yōu)點:精確。缺點:成本高霎桅。
HotSpot使用的是第二種方法栖疑。它有兩類計數(shù)器:方法調用計數(shù)器和回邊計數(shù)器。
方法調用計數(shù)器:統(tǒng)計方法被調用的次數(shù)滔驶,默認閾值Client下1500次遇革,Server下10000次。
如果不做任何設置瓜浸,方法調用計數(shù)器統(tǒng)計的不是方法調用的絕對次數(shù)澳淑,而是一段時間內的次數(shù),超過一定時間后插佛,計數(shù)器會減半杠巡。這種衰減實在GC時順便進行的」涂埽可以用-XX:CounterDecay設置是否衰減氢拥,如果不衰減蚌铜,那么隨著時間的推移,總會達到次數(shù)進行編譯嫩海。
回邊計數(shù)器:統(tǒng)計一個方法中循環(huán)體執(zhí)行的次數(shù)冬殃。準確的說是回邊次數(shù),空循環(huán)不會回邊叁怪,只跳轉到自己审葬。
client模式閾值:方法調用計數(shù)器閾值OSR比率/100,OSR比率默認933奕谭。
server模式閾值:方法調用計數(shù)器閾值(OSR比率 - 解釋器監(jiān)控比率)/100涣觉,OSR比率默認140,解釋器監(jiān)控比率默認33血柳。
回邊計數(shù)器沒有熱度衰減官册。
4. 一些編譯技術
- 語言無關的經(jīng)典優(yōu)化技術之一:公共子表達式消除。
- 語言相關的經(jīng)典優(yōu)化技術之一:數(shù)組范圍檢查消除难捌。
- 最重要的優(yōu)化技術之一:方法內聯(lián)膝宁。
- 最前沿的優(yōu)化技術之一:逃逸分析。
公共子表達式消除:如果一個表達式前面已經(jīng)計算過了根吁,后面表達式的變量也沒有變化過员淫,這個表達式就是公共子表達式,就沒有必要計算了击敌。
//javac不會作任何優(yōu)化满粗,但是JIT會優(yōu)化b * c
int d = (c * b) * 12 + a + +(a + b * c)
數(shù)組邊界檢查消除:Java在訪問數(shù)組元素時會自動進行上下界的范圍檢查,越界則拋出ArrayIndexOutOfBoundsException愚争,但是這也是一種性能負擔映皆。虛擬機根據(jù)情況在編譯器判斷是否可能越界,如果不越界執(zhí)行時就不需要檢查了轰枝。
類似情況還有NullPointException捅彻,除數(shù)為0異常等。
方法內聯(lián):編譯器最重要的優(yōu)化手段之一鞍陨,消除了方法調用的成本步淹,還為其它優(yōu)化建立了基礎。
- 方法內聯(lián)不是代碼復制那么簡單诚撵,因為Java的方法(除了編譯期解析的)缭裆,編譯期都不能確定版本,運行期才可以寿烟。采用“類型繼承關系分析CHA”解決這一問題澈驼。
類型繼承關系分析CHA:基于整個應用,確定目前已加載的類中筛武,某個接口是否有多于一種實現(xiàn)缝其,某個類是否有子類挎塌,子類是否為抽象類等信息。
- 編譯器進行內聯(lián)時内边,如果是非虛方法榴都,那么直接內聯(lián)。如果遇到虛方法漠其,則查詢CHA是否有多個版本嘴高,如果只有一個,那么進行內聯(lián)(激進的和屎,需要逃生門)阳惹。如果后續(xù)虛擬機沒有加載其它類改變繼承關系,則一直內聯(lián)眶俩,否則退回解釋狀態(tài),或重新編譯快鱼。
- 如果CHA查詢出多個版本颠印,編譯器會使用內聯(lián)緩存。在未發(fā)生調用前抹竹,緩存為空线罕,發(fā)生調用后,緩存記錄下方法版本信息窃判,以后每次調用都比較版本钞楼,如果一直,內聯(lián)繼續(xù)袄琳,如果不一致询件,取消內聯(lián)。查找虛方法表唆樊。
逃逸分析:分析對象的動態(tài)作用域宛琅。不是代碼優(yōu)化手段,而是為其它手段提供依據(jù)逗旁。
- 方法逃逸:一個對象被外部方法引用嘿辟,如傳參。
- 線程逃逸:對象被外部線程訪問到片效,如賦值給類變量红伦。
如果證明一個對象不會逃逸,那么可以進行一些高效的優(yōu)化淀衣。
棧上分配:一般對象都在堆上分配昙读,各個線程共享,GC回收內存需要耗費時間膨桥。如果一個對象確定不會逃逸出方法箕戳,比如局部變量某残,那么分配在棧上就很舒服,可以隨棧幀出棧而銷毀陵吸。GC壓力減小玻墅。
同步消除:如果一個對象確定不是線程逃逸,那么就不會被其它線程訪問壮虫,就不存在競爭澳厢,完全可以消除同步。
標量替換:如果一個對象確定不會被外部訪問囚似,那么真正執(zhí)行的時候就不需要創(chuàng)建這個對象剩拢,改為在棧上創(chuàng)建對象拆散后的標量。