早期(編譯期)優化——Javac編譯器
文章目錄
“編譯期”是一個不確定的過程:
- 前端編譯器:把.java轉換為.class的過程。如Sun的javac、Eclispe JDT的ECJ
- 後端執行期間編譯器:將.class轉換為機器碼的過程。Hotspot VM的C1、C2編譯器。
- 靜態提前編譯器:直接把.java轉換為本地機器碼的過程。CNU Compiler for the Java(GCJ)、Excelsior JET。
一、Javac的原始碼與除錯
Javac的原始碼存放在
JDK_SRC_HOME/langtools/src/share/classes/com/sun/tools/j>avac中。還有javac不像hotspot那樣是用C++實現的,javac是純>java程式碼實現的。
編譯過程:
JDK7中Javac編譯過程的主體程式碼(編譯動作的入口是som.sun.tools.javac…main.JavaCompiler):
compile
try {
//準備過程:初始化插入式註解處理器
initProcessAnnotations(processors);
// These method calls must be chained to avoid memory leaks
delegateCompiler =
processAnnotations( //2:執行註解過程
enterTrees(stopIfError(CompileState.PARSE,//過程1.2:輸入到符號表
parseFiles(sourceFileObjects))),//過程1.1:詞法分析、語法分析
classnames);
delegateCompiler.compile2();//過程3:分析及位元組碼生成
delegateCompiler.close();
elapsed_msec = delegateCompiler.elapsed_msec;
}
compile2():
case BY_TODO:
while (!todo.isEmpty())
generate(desugar(flow(attribute(todo.remove()))));//過程3.1-attribute:標註;過程3.2-flow:資料流分析;過程3.3-desugar:解語法糖;過程3.4-generate:生成位元組碼
break;
二、解析與填充符號表
2.1 詞法分析、語法分析
詞法、語法分析對應parseFiles()。
詞法分析:
詞法分析就是將原始碼轉為(Token)標記,單個字元是程式編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵詞、變數名、字面量、運算子都可以成為標記。
語法分析:
就是根據Token構造抽象語法樹的過程,語法樹的每一個節點都代表著程式中的一個語法結構,例如包、型別、修飾符、運算子、介面、返回值甚至程式碼註釋。
2.2 填充符號表
填充符號表對應enterTrees()。
符號表是由一組符號地址和符號資訊構成的表格。符號表在編譯的不同階段都要用到。
三、註解處理器
註解與程式碼一樣都是在執行期間發揮作用的。插入式註解處理器可以在編譯期間對註解進行處理。其工作原理如下:
如果在註解處理期間對語法書進行了修改,那麼編譯器將進行一次迴圈Round,回到解析及填充符號表階段。直到註解處理期間不在修改語法樹。
插入式註解處理器的初始化是在==initProcessAnnotations()中完成的,而註解處理過程是在processAnnotations()==方法中完成的。
四、語義分析與位元組碼生成
語法樹表示一個結構正確的程式的抽象,但無法保證源程式是符合邏輯的。所以就有了語義分析。語義分析包括標註檢查、資料及控制流分析。
例子:
int a=1;
boolean b=false;
char c=2;
int d=a+c; //1
int d=b+c;//2
char d=a+c;//3
從語法、詞法分析中,這是沒毛病的,但是不和邏輯啊!!!1,2,3中只有1在語義上沒毛病。
4.1 標註檢查
對應的是attribute()。
標註檢查:
標註檢查的內容有檢查變數在使用前是否已被宣告、變數與賦值型別是否匹配等。
標註檢查中,還有一個重要的動作——常量摺疊。例子:
int a=1+2;
這個在詞法解析、語法解析之後的語法樹中,'1’、‘+’、‘2’是存在的。然而在標註檢查時,經過常量摺疊後,會被摺疊為字面量‘3’。
4.2 資料及控制流分析
對應的是flow()。
資料及控制流解析:
這是對程式上下文邏輯更近一步的驗證。它可以檢查出程式區域性變數在使用前是否被賦值、方法的每條路徑是否有返回值、是否所有的可檢查異常都被正確處理等問題。
前端編譯器和後端執行期間編譯器在資料及控制流分析的目的上是一致的,但校驗範圍有所區別。有一些校驗項只有在編譯期或執行期才能進行。例子:
//帶有final
public void foo(final int arg){
final int var=0;
}
與
//沒有final修飾
public void foo(int arg){
int var=0;
}
這兩段程式碼編譯出來的class檔案是沒有區別的。區域性變數宣告為final,對執行是沒有影響的,變數的不變性由編譯器在編譯期保障。
4.3 解語法糖
對應的是desugar()。
語法糖只是讓程式設計師更好的編寫程式碼。Java中最常見的語法糖就是泛型、變長引數、自動裝箱/拆箱。虛擬機器執行時並不支援這些語法。所以在編譯階段還原會簡單的基礎語法結構這一過程就是解語法糖。
4.4 位元組碼生成
對應generate()。
位元組碼生成:
除了將前面各個步奏生成的資訊(語法樹、符號表)轉換為位元組碼寫到磁碟中,編譯器還進行了少量的程式碼新增(如例項構造器<init>()、類構造器<clinit>)和轉化工作(如把字串的加操作轉化為StringBuffer或StringBuilder的append()操作)。