1. 程式人生 > >JVM系列第4講:從原始碼到機器碼,發生了什麼?

JVM系列第4講:從原始碼到機器碼,發生了什麼?

在上篇文章我們聊到,無論什麼語言寫的程式碼,其到最後都是通過機器碼執行的,無一例外。那麼對於 Java 語言來說,其從原始碼到機器碼,這中間到底發生了什麼呢?這就是今天我們要聊的。

如下圖所示,編譯器可以分為:前端編譯器、JIT 編譯器和AOT編譯器。下面我們逐個講解。

前端編譯器:原始碼到位元組碼

之前我們說到:對於 Java 虛擬機器來說,其實際輸入的是位元組碼檔案,而不是 Java 檔案。那麼對於 Java 語言而言,其實怎麼將 Java 程式碼轉化成位元組碼檔案的呢?我們知道在 JDK 的安裝目錄裡有一個 javac 工具,就是它將 Java 程式碼翻譯成位元組碼,這個工具我們叫做編譯器。相對於後面要講的其他編譯器,其因為處於編譯的前期,因此又被成為前端編譯器。

通過 javac 編譯器,我們可以很方便地將 java 原始檔翻譯成位元組碼檔案。就拿我們最熟悉的 Hello World 作為例子:

public class Demo{
   public static void main(String args[]){
        System.out.println("Hello World!");
   }
}

我們使用 javac 命令編譯上面這個類,便會生成一個 Demo.class 檔案:

> javac Demo.java
> ls 
Demo.java Demo.class

我們使用純文字編輯器開啟 Demo.class 檔案,我們會發現是一連串的 16 進位制二進位制流。

我們執行 javac 命令的過程,其實就是 javac 編譯器解析 Java 原始碼,並生成位元組碼檔案的過程。說白了,其實就是使用 javac 編譯器把 Java 語言規範轉化為位元組碼語言規範。javac 編譯器的處理過程可以分為下面四個階段:

第一個階段:詞法、語法分析。在這個階段,JVM 會對原始碼的字元進行一次掃描,最終生成一個抽象的語法樹。簡單地說,在這個階段 JVM 會搞懂我們的程式碼到底想要幹嘛。就像我們分析一個句子一樣,我們會對句子劃分主謂賓,弄清楚這個句子要表達的意思一樣。

第二個階段:填充符號表。我們知道類之間是會互相引用的,但在編譯階段,我們無法確定其具體的地址,所以我們會使用一個符號來替代。在這個階段做的就是類似的事情,即對抽象的類或介面進行符號填充。等到類載入階段,JVM 會將符號替換成具體的記憶體地址。

第三個階段:註解處理。我們知道 Java 是支援註解的,因此在這個階段會對註解進行分析,根據註解的作用將其還原成具體的指令集。

第四個階段:分析與位元組碼生成。到了這個階段,JVM 便會根據上面幾個階段分析出來的結果,進行位元組碼的生成,最終輸出為 class 檔案。

我們一般稱 javac 編譯器為前端編譯器,因為其發生在整個編譯的前期。常見的前端編譯器有 Sun 的 javac,Eclipse JDT 的增量式編譯器(ECJ)。

JIT 編譯器:從位元組碼到機器碼

當原始碼轉化為位元組碼之後,其實要執行程式,有兩種選擇。一種是使用 Java 直譯器解釋執行位元組碼,另一種則是使用 JIT 編譯器將位元組碼轉化為本地機器程式碼。

這兩種方式的區別在於,前者啟動速度快但執行速度慢,而後者啟動速度慢但執行速度快。至於為什麼會這樣,其原因很簡單。因為直譯器不需要像 JIT 編譯器一樣,將所有位元組碼都轉化為機器碼,自然就少去了優化的時間。而當 JIT 編譯器完成第一次編譯後,其會將位元組碼對應的機器碼儲存下來,下次可以直接使用。而我們知道,機器碼的執行效率肯定是高於 Java 直譯器的。所以在實際情況中,為了執行速度以及效率,我們通常採用兩者相結合的方式進行 Java 程式碼的編譯執行。

在 HotSpot 虛擬機器內建了兩個即時編譯器,分別稱為 Client Compiler 和Server Compiler。這兩種不同的編譯器衍生出兩種不同的編譯模式,我們分別稱之為:C1 編譯模式,C2 編譯模式。

注意:現在許多人習慣上將 Client Compiler 稱為 C1 編譯器,將 Server Compiler 稱為 C2 編譯器,但在 Oracle 官方文件中將其描述為 compiler mode(編譯模式)。所以說 C1 編譯器、C2 編譯器只是我們自己的習慣性稱呼,並不是官方的說法。這點需要特別注意。

那麼 C1 編譯模式和 C2 編譯模式有什麼區別呢?

C1 編譯模式會將位元組碼編譯為原生代碼,進行簡單、可靠的優化,如有必要將加入效能監控的邏輯。而 C2 編譯模式,也是將位元組碼編譯為原生代碼,但是會啟用一些編譯耗時較長的優化,甚至會根據效能監控資訊進行一些不可靠的激進優化。

簡單地說 C1 編譯模式做的優化相對比較保守,其編譯速度相比 C2 較快。而 C2 編譯模式會做一些激進的優化,並且會根據效能監控做針對性優化,所以其編譯質量相對較好,但是耗時更長。

那麼到底應該選擇 C1 編譯模式還是 C2 編譯模式呢?

實際上對於 HotSpot 虛擬機器來說,其一共有三種執行模式可選,分別是:

  • 混合模式(Mixed Mode) 。即 C1 和 C2 兩種模式混合起來使用,這是預設的執行模式。如果你想單獨使用 C1 模式或 C2 模式,使用 -client-server 開啟即可。
  • 解釋模式(Interpreted Mode)。即所有程式碼都解釋執行,使用 -Xint 引數可以開啟這個模式。
  • 編譯模式(Compiled Mode)。 此模式優先採用編譯,但是無法編譯時也會解釋執行,使用 -Xcomp 開啟這種模式。

在命令列中輸入 java -version 可以看到,我機器上的虛擬機器使用 Mixed Mode 執行模式。

寫到這裡,我們瞭解了從 Java 原始碼到位元組碼,再從位元組碼到機器碼的全過程。本來到這裡就應該結束了,但在我們 Java 中還有一個 AOT 編譯器,它能直接將原始碼轉化為機器碼。

AOT 編譯器:原始碼到機器碼

AOT 編譯器的基本思想是:在程式執行前生成 Java 方法的原生代碼,以便在程式執行時直接使用原生代碼。

但是 Java 語言本身的動態特性帶來了額外的複雜性,影響了 Java 程式靜態編譯程式碼的質量。例如 Java 語言中的動態類載入,因為 AOT 是在程式執行前編譯的,所以無法獲知這一資訊,所以會導致一些問題的產生。類似的問題還有很多,這裡就不一一舉例了。

總的來說,AOT 編譯器從編譯質量上來看,肯定比不上 JIT 編譯器。其存在的目的在於避免 JIT 編譯器的執行時效能消耗或記憶體消耗,或者避免解釋程式的早期效能開銷。

在執行速度上來說,AOT 編譯器編譯出來的程式碼比 JIT 編譯出來的慢,但是比解釋執行的快。而編譯時間上,AOT 也是一個始終的速度。所以說,AOT 編譯器的存在是 JVM 犧牲質量換取效能的一種策略。就如 JVM 其執行模式中選擇 Mixed 混合模式一樣,使用 C1 編譯模式只進行簡單的優化,而 C2 編譯模式則進行較為激進的優化。充分利用兩種模式的優點,從而達到最優的執行效率。

總結

在 JVM 中有三個非常重要的編譯器,它們分別是:前端編譯器、JIT 編譯器、AOT 編譯器。

前端編譯器,最常見的就是我們的 javac 編譯器,其將 Java 原始碼編譯為 Java 位元組碼檔案。JIT 即時編譯器,最常見的是 HotSpot 虛擬機器中的 Client Compiler 和 Server Compiler,其將 Java 位元組碼編譯為本地機器程式碼。而 AOT 編譯器則能將原始碼直接編譯為本地機器碼。這三種編譯器的編譯速度和編譯質量如下:

  • 編譯速度上,解釋執行 > AOT 編譯器 > JIT 編譯器。
  • 編譯質量上,JIT 編譯器 > AOT 編譯器 > 解釋執行。

而在 JVM 中,通過這幾種不同方式的配合,使得 JVM 的編譯質量和執行速度達到最優的狀態。

參考資料

JVM系列目錄


如果只是看,其實無法真正學會知識的。為了幫助大家更好地學習,我建了一個虛擬機器群,專門討論學習 Java 虛擬機器方面的內容,每週針對我所發文章進行討論答疑。如果你有興趣,關注「Java技術精選」公眾號,通過右下角選單「入群交流」加我好友,小助手會拉你入群。