一、概述
如今,基於物理機、Java虛擬機或者是非 Java 的其他高級語言虛擬機(HLLVM)的語言,大多數都遵循如下現代經典編譯原理的思路,在執行前先對程序源碼進行詞法分析和語法分析處理,把源碼轉化為抽象語法樹。對於一門具體語言的實現來說,詞法和語法分析乃至后面的優化器和目標代碼生成器都可以選擇獨立於執行引擎,形成一個完整意義的編譯器去實現,這類代表是 C/C++ 語言。也可以選擇把其中一部分步驟(如生成抽象語法樹之前的步驟)實現為一個半獨立的編譯器,這類代表是 Java 語言(以下介紹的 javac 編譯器)。又或者把這些步驟和執行引擎全部集中封裝在一個封閉的黑匣子之中,如大多數的 JavaScript 語言。
我們都知道 *.java 文件要首先被編譯成 *.class 文件才能被 JVM 認識,這部分的工作主要由 Javac 來完成,類似於 Javac 這樣的我們稱之為前端編譯器;
但是 *.class 文件也不是機器語言,怎么才能讓機器識別呢?就需要 JVM 將 *.class 文件編譯成機器碼,這部分工作由JIT 編譯器完成;
除了這兩種編譯器,還有一種直接把 *.java 文件編譯成本地機器碼的編譯器,我們稱之AOT 編譯器。
AOT 編譯器一直以來都沒有掀起什么大風浪,直到 JDK9 中出現的 Jaotc 提前編譯器,這是一個基於 Graal 編譯器實現的新工具,目的是讓用戶可以針對目標機器,為應用程序進行提前編譯。HotSpot 運行時可以直接加載這些編譯的結果,實現加快程序啟動速度,減少程序達到全速運行狀態的時間。但是提前編譯器無可避免的具有破壞平台中立性、導致字節膨脹等特點,盡管如此,提前編譯無疑已經成為了一種極限榨取性能的手段,且被官方 JDK 關注,可以預想未來會有一個好的發展。
此外,由於 Jaotc 是基於 Graal 編譯器開發的,所以現在 ZGC 和 Shenandoah 收集器還不支持 Graal 編譯器,自然它們在 Jaotc 上也是無法使用的。事實上,目前 Jaotc 只支持 G1 和 Parallel 兩種垃圾收集器。
二、javac 的編譯過程
首先,我們先導一份 javac 的源碼(基於 openjdk8)出來,下載地址:https://hg.openjdk.java.net/jdk8/jdk8/langtools/archive/tip.tar.gz,然后將 JDK_SRC_HOME/langtools/src/share/classes/com/sun 目錄下的源文件全部復制到工程的源碼目錄中,生成的 目錄 如下:
我們執行 com.sun.tools.javac.Main 的 main 方法,就和我們在命令窗口中使用 javac 命令一樣:
從 Sun Javac 的代碼來看,編譯過程大致可以分為三個步驟:
- 解析和填充符號表過程
- 插入式注解處理器的注解處理過程
- 分析和字節碼生成過程
這三個步驟所做的工作內容大致如下:
這三個步驟之間的關系和交互順序如下圖所示,可以看到如果注解處理器在處理注解期間對語法樹進行了修改,編譯器將回到解析和填充符號表的過程進行重新處理,直到注解處理器沒有再對語法樹進行修改為止。
Javac 編譯的入口是 com.sun.tools.javac.main.JavaCompiler 類,上述三個步驟的代碼都集中在這個類的 compile() 和 compile2() 中: