JIT原理


本文轉載自JVM雜談之JIT

導語

JIT技術是JVM中最重要的核心模塊之一。我的課程里本來沒有計划這一篇,但因為不斷有朋友問起,Java到底是怎么運行的?既然Hotspot是C++寫的,那Java是不是可以說運行在C++之上呢?為了澄清這些概念,我才想起來了加了這樣一篇文章,算做番外篇吧。

Just In Time

Just in time編譯,也叫做運行時編譯,不同於 C / C++ 語言直接被翻譯成機器指令,javac把java的源文件翻譯成了class文件,而class文件中全都是Java字節碼。那么,JVM在加載了這些class文件以后,針對這些字節碼,逐條取出,逐條執行,這種方法就是解釋執行。

還有一種,就是把這些Java字節碼重新編譯優化,生成機器碼,讓CPU直接執行。這樣編出來的代碼效率會更高。通常,我們不必把所有的Java方法都編譯成機器碼,只需要把調用最頻繁,占據CPU時間最長的方法找出來將其編譯成機器碼。這種調用最頻繁的Java方法就是我們常說的熱點方法(Hotspot,說不定這個虛擬機的名字就是從這里來的)。

這種在運行時按需編譯的方式就是Just In Time。

主要技術點

其實JIT的主要技術點,從大的框架上來說,非常簡單,就是申請一塊既有寫權限又有執行權限的內存,然后把你要編譯的Java方法,翻譯成機器碼,寫入到這塊內存里。當再需要調用原來的Java方法時,就轉向調用這塊內存。

我們看一個例子:

#include<stdio.h>

int inc(int a) {
    return a + 1;
}

int main() {
    printf("%d\n", inc(3));
    return 0;
}

上面這個例子很簡單,就是把3加1,然后打印出來,我們通過以下命令,查看一下它的機器碼:

# gcc -o inc inc.c
# objdump -d inc

然后在這一堆輸出中,可以找到 inc 方法最終被翻譯成了這樣的機器碼:

  40052d:	55                   	push   %rbp
  40052e:	48 89 e5             	mov    %rsp,%rbp
  400531:	89 7d fc             	mov    %edi,-0x4(%rbp)
  400534:	8b 45 fc             	mov    -0x4(%rbp),%eax
  400537:	83 c0 01             	add    $0x1,%eax
  40053a:	5d                   	pop    %rbp
  40053b:	c3                   	retq 

我來解釋一下(讀者需要一定的x86匯編語言的知識)。

第一句,保存上一個棧幀的基址,並把當前的棧指針賦給棧基址寄存器,這是進入一個函數的常規操作。我們不去管它。

第三句,把edi存到棧上。在x64處理器上,前6個參數都是使用寄存器傳參的。第一個參數會使用rdi,第二個參數使用 rsi,等等。所以 edi 里存的其實就是第一個參數,也就是整數 3,為什么使用rdi的低32位,也就是 edi 呢?因為我們的入參 a 是 int 型啊。大家可以換成 long 型看看效果。

第四句,把上一步存到棧上的那個整數再存進 eax 中。

第五句往后,把 eax 加上 1, 然后就退棧,返回。按照x64的規定(ABI),返回值通過eax傳遞。

我們看到了,其實第三句,第四句好像根本沒有存在的必要,gcc 默認情況下,生成的機器碼有點傻,它總要把入參放到棧上,但其實,我們是可以直接把參數從 rdi 中放入到 rax 中的。不滿意。那我們可以自己改一下,讓它更精簡一點。怎么做呢?答案就是運行時修改 inc 的邏輯。

#include<stdio.h>
#include<memory.h>
#include<sys/mman.h>

typedef int (* inc_func)(int a); 

int main() {
    char code[] = { 
        0x55,             // push rbp
        0x48, 0x89, 0xe5, // mov rsp, rbp
        0x89, 0xf8,       // mov edi, eax
        0x83, 0xc0, 0x01, // add $1, eax
        0x5d,             // pop rbp
        0xc3              // ret
    };  

    void * temp = mmap(NULL, sizeof(code), PROT_WRITE | PROT_EXEC,
            MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); 

    memcpy(temp, code, sizeof(code));
    inc_func p_inc = (inc_func)temp;
    printf("%d\n", p_inc(7));

    return 0;
}

在這個例子中,我們使用了 mmap 來申請了一塊有寫權限和執行權限的內存,然后把我們手寫的機器碼拷進去,然后使用一個函數指針指向這塊內存,並且調用它。通過這種方式我們就可以執行這一段手寫的機器碼了。

運行一下看看:

# gcc -o inc inc.c 
# ./inc
8

再回想一下這個過程。我們通過手寫機器碼把原來的 inc 函數代替掉了。在新的例子中,我們是使用程序中定義的數據來重新造了一個 inc 函數。這種在運行的過程創建新的函數的方式,就是JIT的核心操作。

解釋器,C1和C2

在Hotspot中,解釋器是為每一個字節碼生成一小段機器碼,在執行Java方法的過程中,每次取一條指令,然后就去執行這一個指令所對應的那一段機器碼。256條指令,就組成了一個表,在這個表里,每一條指令都對應一段機器碼,當執行到某一條指令時,就從這個表里去查這段機器碼,並且通過 jmp 指令去執行這段機器碼就行了。

這種方式被稱為模板解釋器。

模板解釋器生成的代碼有很多冗余,就像我們上面的第一個例子那樣。為了生成更精簡的機器碼,我們可以引入編譯器優化手段,例如全局值編碼,死代碼消除,標量展開,公共子表達式消除,常量傳播等等。這樣生成出來的機器碼會更加優化。

但是,生成機器碼的質量越高,所需要的時間也就越長。JIT線程也是要擠占Java 應用線程的資源的。所以C1是一個折衷,編譯時間既不會太長,生成的機器碼的指令也不是最優化的,但肯定比解釋器的效率要高很多。

如果一個Java方法調用得足夠頻繁,那就更值得花大力氣去為它生成更優質的機器碼,這時就會觸發C2編譯,c2是一個運行得更慢,但卻能生成更高效代碼的編譯器。

由此,我們看到,其實Java的運行,幾乎全部都依賴運行時生成的機器碼上。所以,對於文章開頭的那個問題“Java是運行在C++上的嗎?”,大家應該都有自己的答案了。這個問題無法簡單地回答是或者不是,正確答案就是Java的運行依賴模板解釋器和JIT編譯器。

多說一點優化

我們這節課所舉的例子中,可以做更多的優化,例如,既然我進到inc函數以后,完全沒有使用棧,那其實,我就不要再為它開辟棧幀了。所以可以把push rbp, pop rbp的邏輯都去掉。

進一步優化成這樣:

    char code[] = { 
        0x89, 0xf8,       // mov edi, eax
        0x83, 0xc0, 0x01, // add $1, eax
        0xc3              // ret
    }; 

可以看到,指令更加精簡了。我們重新編譯運行,還是能成功打印出8。

根據這個問題:為什么 lea 會被用來計算?

我們還可以寫出更優化的代碼來:

    char code[] = { 
        0x8d, 0x47, 0x01,    // lea 0x1(rdi), rax
        0xc3                 // ret
    };

如果開啟 gcc 的優化編譯,我們也可以得到這樣的代碼,例如,還是針對這個方法:

int inc(int a) {
    return a + 1;
}

使用 -O2 優化:

# gcc -o inc inc.c -O2
# objdump -d inc

就可以看到,inc 的機器碼變成這樣了:

00000000004005f0 <inc>:
  4005f0:	8d 47 01             	lea    0x1(%rdi),%eax
  4005f3:	c3                   	retq 

這和我們手寫的優化的機器碼是完全一樣的了。

實際上,C1和C2所要做的和gcc的優化編譯是一樣的,就是使用特定的方法生成更高效的機器碼。但是從原理上來說,運行時生成機器碼這個技術,大家都是相通的。

最后,補充一句,iOS禁掉了JIT編譯,所用的手段就是無法申請一塊同時具有寫權限和執行權限的內存。那么,JIT的核心基石,運行時生成可執行的機器碼就無法存在了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM