1. 程式人生 > >理解JIT 編譯器

理解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 需要經過解釋執行,所以它的速度必然比可執行的二進位制位元組碼程式慢。

Created with Raphaël 2.1.0原始碼javac是否需要JIT編譯?JIT編譯器機器碼直譯器yesno

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優化的細節。

  1. 使用暫存器優化
    編譯器通過決定何時從主存取值,何時向暫存器充值,來優化程式執行,減少開銷。
    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值在暫存器裡迴圈,迴圈結束後,將最終的結果從暫存器返回給主存。這樣就省去了從主存中檢索值得開銷。

  2. 使用方法內聯優化
    方法內聯就是把方法的程式碼“複製”到發起呼叫的方法裡,以消除方法呼叫。
    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優化效能提升越大。
jit效能優化