日照充足會讓西瓜更甜,那擁有即時編譯優化會讓Java程序怎麼樣?本文會初步介紹JVM的即時編譯優化特性,而且經過異常堆棧丟失這一常見的現象來進行舉例java
Java程序在運行初期是經過解釋器來執行,當發現某塊代碼運行特別頻繁,就會將之斷定爲熱點代碼(Hot Spot Code), 虛擬機會將這部分代碼編譯成本地機器碼,並對這些代碼進行優化。這件事就是即時編譯(Just In Time, JIT)優化, 作這件事的就是即時編譯器。緩存
目前主流虛擬機都採用解釋器、編譯器並存的架構。架構
由於編譯器存在過分優化,基於假設優化等失敗的優化結果,經過逆優化(Deoptimization)的方式,將程序的執行主動權從編譯器交給解釋器執行。能夠把解釋器當作是一個保守派,編譯器是一個激進派,在JVM執行體系裏,二者相輔相成,互相配合。ide
通常虛擬機都內置了兩個或三個即時編譯器,歷史比較久遠的C1, C2, 以及在JDK10纔出現的Graal模塊化
C1:客戶端編譯器(Client Complier),執行時間較短,啓動程序的時間較快。在一些物聯網小型設備上可指定這種編譯器,經過-client參數強制指定性能
C2:服務端編譯器(Server Complier),執行時間較長,啓動時間較長但可編譯高度優化的代碼,峯值性能更高。可經過-server參數強制指定優化
Graal:是一個實驗性質的即時編譯器,其最大的特色是該編譯器用Java語言編寫,更加模塊化,也更容易開發與維護。充分預熱後Java代碼編譯成二進制碼後其執行性能並不亞於由C++編寫的C2。能夠經過參數 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 啓用,並替換 C2ui
雖然能夠經過-Xint參數強制虛擬機處於"解釋模式"此時編譯器不工做,能夠經過-Xcomp參數強制虛擬機處於"編譯模式"此時解釋器不工做,能夠經過-client參數使C2不工做,也能夠經過-server參數使C1不工做,可是並不推薦這樣作,由於有分層編譯優化這一特性。this
編譯器在編譯代碼的時候會佔用程序運行時間,優化程度越高的代碼編譯時間會越長,甚至會須要解釋器負責收集程序運行監控信息提供給編譯器來編譯優化程度更高的代碼。因此爲了在更短的時間內編譯優化程度更高的代碼,須要編譯器之間的配合,也就是所謂的分層編譯優化。一共有五層,分別是:spa
純解釋執行,解釋器不開啓收集程序運行監控信息
使用C1編譯器進行簡單可靠的優化,解釋器不開啓收集程序運行監控信息
仍然使用C1編譯器優化,可是會針對方法調用次數和回邊次數(循環代碼調用次數)相關的統計
仍然使用C1編譯器優化,統計信息才上一層的基礎上會加上分支跳轉、虛方法調用等所有統計信息,解釋器火力全開
使用C2編譯器優化,相比C1,C2會開啓更多耗時更長的優化,還會根據解釋器提供的程序運行信息進行一些更爲激進的優化
在開啓編譯優化後,熱點代碼可能會被重複編譯,C1編譯器編譯得更快,C2編譯器編譯質量更高,第0層模式解釋器執行的時候也不用收集監控信息,第4層模式C2在進行耗時較長的編譯較爲忙碌時候,C1也能爲C2承擔一部分編譯工做,交互關係以下圖
上面提到即便編譯是針對熱點代碼進行編譯優化,那麼什麼是熱點代碼?
這裏的屢次如何知道具體有多少次?有兩種方法能夠知道
目前HotSpot虛擬機使用的是第二種方法,虛擬機爲每一個方法都準備了兩類計數器,方法調用計數器以及回邊計數器(回邊的意思是在循環的末尾邊界往回跳轉,能夠理解爲循環代碼的一次執行)
講到這裏給你們舉一個工做中常常見到的一個JIT優化案例:異常堆棧丟失
衆所周知在打印Java異常的時候,會將其堆棧信息一併輸出,這些堆棧信息很是重要,有助於咱們排查問題,像這樣
20:10:50.491 [main] ERROR com.yangkw.ErrorTest
java.lang.NullPointerException: null
at com.yangkw.ErrorTest.error(ErrorTest.java:33)
at com.yangkw.ErrorTest.main(ErrorTest.java:19)
複製代碼
可是在最近在觀察系統的線上運行日誌的時候,發現了不少不帶堆棧的異常日誌,讓人摸不着頭腦到底發生了什麼,像這樣
20:10:50.491 [main] ERROR com.yangkw.ErrorTest
java.lang.NullPointerException: null
複製代碼
經過前面關於JIT編譯觸發條件的介紹,能夠設想是拋出異常執行太頻繁因此觸發了JIT優化致使,因而咱們能夠寫一個Demo來驗證,堆棧完整的時候打印"full trace",堆棧丟失的時候打印"no trace"
public static void main(String[] args) throws InterruptedException {
int count = 0;
while (true) {
try {
count++; //統計調用次數
error();
} catch (Exception e) {
if (e.getStackTrace().length == 0) {
LOG.error("no trace count:{}", count, e);
Thread.sleep(1000); //方便觀察日誌
} else {
LOG.error("full trace count:{}", count, e);
}
}
}
}
private static void error() {
String nullMsg = null;
nullMsg.toString();
}
複製代碼
下面是執行結果,能夠看出程序是在執行到8405次(每次執行都會不一樣)的時候丟失了堆棧
雖然8405次執行的時候丟失了堆棧,可是並不能說明是由於JIT優化致使的,因而咱們能夠加上參數-XX:+PrintCompilation 來打印即時編譯狀況。
能夠看到,在10388次執行的時候是有堆棧信息的,在10389次執行的時候就丟失了堆棧信息,在這中間就發生了即便編譯優化,針對這一現象官方術語稱之爲"fast throw"能夠經過參數-XX:-OmitStackTraceInFastThrow關閉這一優化
在ORACLE官方文檔有這麼一段描述
The compiler in the server VM now provides correct stack backtraces for all "cold" built-in exceptions. For performance purposes, when such an exception is thrown a few times, the method may be recompiled. After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. To disable completely the use of preallocated exceptions, use this new flag: -XX:-OmitStackTraceInFastThrow.
堆棧丟失只是表面現象,JIT還對其作了如下優化: