本文會先介紹Java的執行過程,進而引出對即時編譯器的探討,下篇會介紹分層編譯的機制,最後介紹即時編譯器對應用啓動性能的影響。前端
本文內容基於HotSpot虛擬機,設計Java版本的地方會在文中說明。java
Java面試中,有一道面試題是這樣問的:Java程序是解釋執行仍是編譯執行?面試
在咱們剛學習Java時,大概會認爲Java是編譯執行的。其實,Java既有解釋執行,也有編譯執行。後端
Java程序一般的執行過程以下:緩存
源碼.java文件經過javac命令編譯成.class的字節碼,再經過java命令執行。微信
須要說明的是,在編譯原理中,一般將編譯分爲前端和後端。其中前端會對程序進行詞法分析、語法分析、語義分析,而後生成一箇中間表達形式(稱爲IR:Intermediate Representation)。後端再講這個中間表達形式進行優化,最終生成目標機器碼。dom
在Java中,javac以後生成的就是中間表達形式(.class),舉個栗子工具
public class JITDemo2 {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
複製代碼
上述代碼經過javap反編譯後以下:性能
// javap -c JITDemo2.class
Compiled from "JITDemo2.java"
public class com.example.demo.jitdemo.JITDemo2 {
public com.example.demo.jitdemo.JITDemo2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
複製代碼
JVM在執行時,首先會逐條讀取IR的指令來執行,這個過程就是解釋執行的過程。當某一方法調用次數達到即時編譯定義的閾值時,就會觸發即時編譯,這時即時編譯器會將IR進行優化,並生成這個方法的機器碼,後面再調用這個方法,就會直接調用機器碼執行,這個就是編譯執行的過程。學習
因此,從.java文件到最終的執行,其過程大體以下:
(CodeCache會在下文中介紹)
那麼,什麼時候出發即時編譯?即時編譯的過程又是怎樣的?咱們繼續往下研究。
HotSpot虛擬機有兩個編譯器,稱爲C1和C2編譯器(Java10之後新增了一個編譯器Graal)。
C1編譯器對應參數-client,對於執行時間較短,對啓動性能有要求的程序,能夠選擇C1。
C2編譯器對應參數-server,對峯值性能有要求的程序,能夠選擇C2。
但不管是-client仍是-server,C1和C2都是有參與編譯工做的。這種方式成爲混合模式(mixed),也是默認的方式,能夠經過java -version看出:
C:\Users\Lord_X_>java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode) 複製代碼
最後一行的mixed mode說明了這一點。
咱們也能夠經過-Xint參數強行指定只使用解釋模式,此時即時編譯器徹底不參與工做,java -version的最後一行會顯示interpreted mode。
能夠經過參數-Xcomp強行指定只使用編譯模式,此時程序啓動後就會直接對全部代碼進行編譯,這種方式會拖慢啓動時間,但啓動後因爲省去了解釋執行和C一、C2的編譯時間,代碼執行效率會提高不少。此時java -version的最後一行會顯示compiled mode。
下面經過一段代碼來對比一下三種模式的執行效率(一個簡陋的性能 ):
public class JITDemo2 {
private static Random random = new Random();
public static void main(String[] args) {
long start = System.currentTimeMillis();
int count = 0;
int i = 0;
while (i++ < 99999999){
count += plus();
}
System.out.println("time cost : " + (System.currentTimeMillis() - start));
}
private static int plus() {
return random.nextInt(10);
}
}
複製代碼
添加虛擬機參數:-Xint -XX:+PrintCompilation(打印編譯信息)
執行結果:
編譯信息沒有打印出來,側面證實了即時編譯器沒有參與工做。
添加虛擬機參數:-Xcomp -XX:+PrintCompilation
執行結果:
會產生大量的編譯信息
添加虛擬機參數:-XX:+PrintCompilation
執行結果:
結論:耗時由大到小排序爲:純解釋模式 > 純編譯模式 > 混合模式
但這裏只是一個很簡短的程序,若是是長時間運行的程序,不知純編譯模式的執行效率會否高於混合模式,並且這個測試方式並不嚴格,最好的方式應該是在嚴格的基準測試下測試。
即時編譯器觸發的根據有兩個方面:
JVM在調用一個方法時,會在計數器上+1,若是方法裏面有循環體,每次循環,計數器也會+1。
在不啓用分層編譯時(下篇會介紹),當某一方法的計數器達到由參數-XX:CompileThreshold指定的值時(C1爲1500,C2爲10000),就會觸發即時編譯。
下面作個關閉分層編譯時,即時編譯觸發的實驗:
// 參數:-XX:+PrintCompilation -XX:-TieredCompilation(關閉分層編譯)
public class JITDemo2 {
private static Random random = new Random();
public static void main(String[] args) {
long start = System.currentTimeMillis();
int count = 0;
int i = 0;
while (i++ < 15000){
System.out.println(i);
count += plus();
}
System.out.println("time cost : " + (System.currentTimeMillis() - start));
}
// 調用時,編譯器計數器+1
private static int plus() {
return random.nextInt(10);
}
}
複製代碼
執行結果以下:
因爲解釋執行時的計數工做並無嚴格與編譯器同步,因此並不會是嚴格的10000,其實只要調用次數足夠大,就能夠視爲熱點代碼,不必作到嚴格同步。
public class JITDemo2 {
private static Random random = new Random();
public static void main(String[] args) {
long start = System.currentTimeMillis();
plus();
System.out.println("time cost : " + (System.currentTimeMillis() - start));
}
// 調用時,編譯器計數器+1
private static int plus() {
int count = 0;
// 每次循環,編譯器計數器+1
for (int i = 0; i < 15000; i++) {
System.out.println(i);
count += random.nextInt(10);
}
return random.nextInt(10);
}
}
複製代碼
執行結果:
PS:每次方法調用中有10次循環,因此每次方法調用計數器應該+11,因此應該會在差很少大於10000/11=909次調用時觸發即時編譯。
public class JITDemo2 {
private static Random random = new Random();
public static void main(String[] args) {
long start = System.currentTimeMillis();
int count = 0;
int i = 0;
while (i++ < 15000) {
System.out.println(i);
count += plus();
}
System.out.println("time cost : " + (System.currentTimeMillis() - start));
}
// 調用時,編譯器計數器+1
private static int plus() {
int count = 0;
// 每次循環,編譯器計數器+1
for (int i = 0; i < 10; i++) {
count += random.nextInt(10);
}
return random.nextInt(10);
}
}
複製代碼
執行結果:
CodeCache是熱點代碼的暫存區,通過即時編譯器編譯的代碼會放在這裏,它存在於堆外內存。
-XX:InitialCodeCacheSize和-XX:ReservedCodeCacheSize參數指定了CodeCache的內存大小。
PS:能夠經過-XX:+PrintFlagsFinal打印出全部參數的默認值。
能夠經過JDK自帶的jconsole工具看到CodeCache在內存中所處的位置,例如
從圖中曲線圖能夠看出CodeCache已經使用了4M多。
平時咱們爲一個應用分配內存時每每會忽略CodeCache,CodeCache雖然佔用的內存空間不大,並且他也有GC,每每不會被填滿。但若是CodeCache一旦被填滿,那對於一個QPS高的、對性能有高要求的應用來講,能夠說是災難性的。
經過上文的介紹,咱們知道JVM內部會先嚐試解釋執行Java字節碼,當方法調用或循環回邊達到必定次數時,會觸發即時編譯,將Java字節碼編譯成本地機器碼以提升執行效率。這個編譯的本地機器碼是緩存在CodeCache中的,若是有大量的代碼觸發了即時編譯,並且沒有及時GC的話,CodeCache就會被填滿。
一旦CodeCache被填滿,已經被編譯的代碼還會以本地代碼方式執行,但後面沒有編譯的代碼只能以解釋執行的方式運行。
經過第2小節的比較,能夠清晰看出解釋執行和編譯執行的性能差別。因此對於大多數應用來講,這種狀況的出現是災難性的。
CodeCache被填滿時,JVM會打印一條日誌:
JVM針對CodeCache提供了GC方式: -XX:+UseCodeCacheFlushing。在JDK1.7.0_4以後這個參數默認開啓,當CodeCache即將填滿時會嘗試回收。JDK7在這方面的回收作的不是不多,GC收益較低,在JDK8有了很大的改善,因此能夠經過升級到JDK8來直接提高這方面的性能。
那麼何時CodeCache中被編譯的代碼是能夠回收的呢?
這要從編譯器的編譯方式提及。舉個例子,下面這段代碼:
public int method(boolean flag) {
if (flag) {
return 1;
} else {
return 0;
}
}
複製代碼
從解釋執行的角度來看,他的執行過程以下:
但通過即時編譯器編譯後的代碼不必定是這樣,即時編譯器在編譯前會收集大量的執行信息,例如,若是這段代碼以前輸入的flag值都爲true,那麼即時編譯器可能會將他變異成下面這樣:
public int method(boolean flag) {
return 1;
}
複製代碼
即下圖這樣
但可能後面不老是flag=true,一旦flag傳了false,這個錯了,此時編譯器就會將他「去優化」,變成編譯執行方式,在日誌中的表現是made not entrant:
此時該方法不能再進入,當JVM檢測到全部線程都退出該編譯後的made not entrant,會將該方法標記爲:made zombie,此時 這塊代碼佔用的內存就是可回收的了。能夠經過編譯日誌看出:
在Java8中提供了一個JVM啓動參數:-XX:+PrintCodeCache,他能夠在JVM中止時打印CodeCache的使用狀況,能夠在每次中止應用時觀察一下這個值,慢慢調整爲一個最合適的大小。
以一個SpringBoot的Demo說明一下:
// 啓動參數:-XX:ReservedCodeCacheSize=256M -XX:+PrintCodeCache
@RestController
@SpringBootApplication
public class DemoApplication {
// ... other code ...
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
System.out.println("start....");
System.exit(1);
}
}
複製代碼
這裏我將CodeCache定義爲256M,並在JVM退出時打印了CodeCache使用狀況,日誌以下:
最多隻使用了6721K(max_used),浪費了大量的內存,此時就能夠嘗試將-XX:ReservedCodeCacheSize=256M調小,將多餘的內存分配給別的地方。
[1] https://blog.csdn.net/yandaonan/article/details/50844806
[2] 深刻理解Java虛擬機 周志明 第11章
[3] 極客時間《深刻拆解Java虛擬機》 鄭雨迪