理解JIT 編譯器
前言
本文嘗試用淺顯的語言, 解釋JIT的概念和基本原理,讓讀者明白JIT的執行方式和作用。最後,附上關於JIT的程式碼樣例,幫助大家更好理解JIT。本文使用JVM虛擬機器為Hotspot ,一切分析都在Hotpot上。如有不對的地方,歡迎指正。
JIT簡介
JIT 是just in time 的縮寫,即時編譯編譯器。
當JIT編譯啟用時, JVM讀入位元組碼檔案解釋後,將其發給JIT編譯器。JIT編譯器將位元組碼編譯成本機機器程式碼。
java程式碼編譯過程
原始碼經過javac編譯,轉換成java位元組碼檔案(.class檔案)。之後,JVM直譯器將位元組碼檔案翻譯成對應的機器指令,逐條讀入,逐條解釋翻譯。最後,對應的OS執行機器指令。
java code 需要經過解釋執行,所以它的速度必然比可執行的二進位制位元組碼程式慢。
JIT編譯
通常,JVM執行程式碼時, 它並不立即開始JIT編譯。
主要有兩點原因
- 程式碼本身只會被執行一次,那麼從效率上來講,對它進行JIT編譯就是在浪費精力。因為JIT編譯相對於直譯器翻譯位元組碼開銷要大的多。
- 當代碼執行的次數越多, JIT就會越瞭解程式碼結構。JIT對程式碼的優化會更好。
那麼什麼時候會對程式碼進行JIT編譯?
一般來說,當代碼被頻繁呼叫時(通常場景,程式碼中的迴圈體),該程式碼塊將被JIT編譯器編譯。
JIT的編譯閾值
在JVM中有兩個計數器:
- 方法被呼叫的次數
- 方法中迴圈被回彈執行的次數
JVM在執行java方法時,它會堅持這兩個計數器總和來決定是否需要JIT編譯。
*-XX:CompileThreshold=N 可以配置這個閾值。在server模式下,它的預設值是10000;在client模式下,預設值是1500。
PS:不同的模式使用的JIT編譯器是不同的。在client模式下, JVM使用的是一個代號為C1的輕量級編譯器,而在server模式下,使用的是代號C2相對重量級的編譯器。與C1相比,C2編譯的更徹底,所以服務起來後,效能更高 *
2
JIT的優化策略
JIT的優化策略有很多,這裡主要介紹兩種比較普遍的優化,幫助大家理解JIT理解JIT優化的細節。
使用暫存器優化
編譯器通過決定何時從主存取值,何時向暫存器充值,來優化程式執行,減少開銷。
Java Code 的例子public class RegisterTest { private int sum; public void calculateSum(int n) { for (int i = 0; i < n; ++i) { sum += i; } } }
在上面的例子中,sum的值在某些時刻可能在主存中,但是從主存中檢索值是開銷很大的操作。迴圈中可能多次從主存取值,效能很低。JIT編譯器暫存器優化策略,會載入一個暫存器給sum並賦予其初始值,sum值在暫存器裡迴圈,迴圈結束後,將最終的結果從暫存器返回給主存。這樣就省去了從主存中檢索值得開銷。
使用方法內聯優化
方法內聯就是把方法的程式碼“複製”到發起呼叫的方法裡,以消除方法呼叫。
Java Code 的例子
public void caller(){
int a=1;
int b=2;
//do sth
int result =sum(a,b);
}
public int sum(int x,int y){
return x+y;
}
經過JIT編譯器優化後,可能是轉換為
public void caller(){
int a=1;
int b=2;
//do sth
int result =a+b;
}
PS:以上例子只是幫助理解,並不能準確的反映JIT的方法內聯優化。
JIT編譯器優化實踐
下面是一個簡單的JIT優化的例子
package test.jit;
/**
* -XX:CompileThreshold=100000
* @author marshall
*
*/
public class JITTest {
public static int sum(int x, int y) {
int a = x + 1;
int b = y + 1;
int rs = a + b;
return rs;
}
public static int total(int n) {
int res = 0;
for (int i = 0; i < n; i++) {
res += sum(i, i);
}
return res;
}
public static void main(String[] args) {
int rs;
long bf;
long af;
long beforeJIT;
long afterJIT;
bf = System.nanoTime();
rs = total(100000);
af = System.nanoTime();
beforeJIT = af - bf;
System.out.println("程式沒有經過預熱即沒有經過JIT優化,花費時間:" + beforeJIT + " 納秒");
bf = System.nanoTime();
rs = total(100000);
af = System.nanoTime();
afterJIT = af - bf;
System.out.println("程式經過預熱即經過JIT優化,花費時間:" + afterJIT + " 納秒");
System.out.println("減少開銷:" + (beforeJIT - afterJIT) / (beforeJIT * 1.0));
}
}
手動設定程式閾值為100000,經過多次測試,執行測試越多, JIT優化效能提升越大。