JVM-10.2-Javac編譯器

10-2 Javac編譯器

1 概述

Java語言的“編譯期”其實是一段“不確定”的操作過程,因爲它可能是指一個前端編譯器
(其實叫“編譯器的前端”更準確一些)把*.java文件轉變成*.class文件的過程;也可能是指虛
擬機的後端運行期編譯器(JIT編譯器,Just In Time Compiler)把字節碼轉變成機器碼的過
程;還可能是指使用靜態提前編譯器(AOT編譯器,Ahead Of Time Compiler)直接把*.java
文件編譯成本地機器代碼的過程。下面列舉了這3類編譯過程中一些比較有代表性的編譯
器。

  • 前端編譯器:Sun的Javac 、Eclipse JDT中的增量式編譯器(ECJ)
  • JIT編譯器:HotSpot VM的C1、C2編譯器
  • AOT編譯器:GNU Compiler for the Java(GCJ)、Excelsior JET

2 Javac的編譯過程

  • 解析與填充符號表過程。
  • 插入式註解處理器的註解處理過程。
  • 分析與字節碼生成過程。

這3個步驟之間的關係與交互順序如圖10-4所示。
在這裏插入圖片描述

3 解析與填充符號表

解析步驟包括了經典程序編譯原理中的詞法分析和語法分析兩個過程

3.1 詞法分析

詞法分析是將源代碼的字符流轉變爲標記(Token)集合,單個字符是程序編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵字、變量名、字面量、運算符都可以成爲標記

3.2 語法分析

語法分析是根據Token序列構造抽象語法樹的過程,語法樹的每一個節點都代表着程序代碼中的一個語法結構(Construct),例如包、類型、修飾符、運算符、接口、返回值甚至代碼註釋等都可以是一個語法結構。

3.3 填充符號表

完成了語法分析和詞法分析之後,下一步就是填充符號表的過程。符號表(Symbol Table)是由一組符號地址和符號信息構成的表格。在語義分析中,符號表所登記的內容將用於語義檢查(如檢查一個名字的使用和原先的說明是否一致)和產生中間代碼。在目標代碼生成階段,當對符號名進行地址分配時,符號表是地址分配的依據。

4 註解處理器

註解(Annotation)與普通的Java代碼一樣,是在運行期間發揮作用的。在JDK 1.6中實現了JSR-269規範,提供了一組插入式註解處理器的標準API在編譯期間對註解進行處理,我們可以把它看做是一組編譯器的插件,在這些插件裏面,可以讀取、修改、添加抽象語法樹中的任意元素。如果這些插件在處理註解期間對語法樹進行了修改,編譯器將回到解析及填充符號表的過程重新處理,直到所有插入式註解處理器都沒有再對語法樹進行修改爲止,每一次循環稱爲一個Round,也就是圖10-4中的迴環過程。

5 語義分析與字節碼生成

語法分析之後,編譯器獲得了程序代碼的抽象語法樹表示,語法樹能表示一個結構正確的源程序的抽象,但無法保證源程序是符合邏輯的。而語義分析的主要任務是對結構上正確的源程序進行上下文有關性質的審查,如進行類型審查。
Javac的編譯過程中,語義分析過程分爲標註檢查以及數據及控制流分析兩個步驟

5.1 標註檢查

標註檢查步驟檢查的內容包括諸如變量使用前是否已被聲明、變量與賦值之間的數據類型是否能夠匹配等。

常量摺疊

在標註檢查步驟中,還有一個重要的動作稱爲常量摺疊,如果我們在代碼中寫了如下定義:

int a=1+2;

那麼在語法樹上仍然能看到字面量“1”、“2”以及操作符“+”,但是在經過常量摺疊之後,它們將會被摺疊爲字面量“3”。這個插入式表達式(InfixExpression)的值已經在語法樹上標註出來了(ConstantExpressionValue:3)。由於編譯期間進行了常量摺疊,所以在代碼裏面定義“a=1+2”比起直接定義“a=3”,並不會增加程序運行期哪怕僅僅一個CPU指令的運算量。

5.2 數據及控制流分析

數據及控制流分析是對程序上下文邏輯更進一步的驗證,它可以檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問題。編譯時期的數據及控制流分析與類加載時的數據及控制流分析的目的基本上是一致的,但校驗範圍有所區別,有一些校驗項只有在編譯期或運行期才能進行。

局部變量字段(實例變量、類變量)是有區別的,它在常量池中沒有CONSTANT_Fieldref_info的符號引用,自然就沒有訪問標誌(Access_Flags)的信息,甚至可能連名稱都不會保留下來(取決於編譯時的選項),自然在Class文件中不可能知道一個局部變量是不是聲明爲final了。因此,將局部變量聲明爲final,對運行期是沒有影響的,變量的不變性僅僅由編譯器在編譯期間保障。

5.3 解語法糖

Java中最常用的語法糖主要是前面提到過的泛型(泛型並不一定都是語法糖實現,如C#的泛型就是直接由CLR支持的)、變長參數、自動裝箱/拆箱等,虛擬機運行時不支持這些語法,它們在編譯階段還原回簡單的基礎語法結構,這個過程稱爲解語法糖。

5.4 字節碼生成

字節碼生成是Javac編譯過程的最後一個階段,字節碼生成階段不僅僅是把前面各個步驟所生成的信息(語法樹、符號表)轉化成字節碼寫到磁盤中,編譯器還進行了少量的代碼添加和轉換工作,包括:

  • 將實例構造器<init>()方法和類構造器<clinit>()方法添加到語法樹之中
  • 其他的一些代碼替換工作用於優化程序的實現邏輯,如把字符串的加操作替換爲StringBuffer或StringBuilder(取決於目標代碼的版本是否大於或等於JDK 1.5)的append()操作等

完成了對語法樹的遍歷和調整之後,就會把填充了所有所需信息的符號表交給com.sun.tools.javac.jvm.ClassWriter類,由這個類的writeClass()方法輸出字節碼,生成最終的Class文件,到此爲止整個編譯過程宣告結束。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章