Java「編譯期」是一段「不肯定」的操做過程:多是指一個前端編譯器(編譯器的前端)把*.java文件轉變爲*.class文件的過程;多是指虛擬機的後端運行期編譯器(JIT編譯器,Just In Time Compiler)把字節碼轉變爲機器碼的過程;多是指使用靜態提早編譯器(AOT編譯器,Ahead Of Time Compiler)直接把*.java文件編譯成本地機器代碼的過程。這三類編譯過程當中一些比較有表明性的編譯器:javascript
前端編譯期:Sun的Javac/EclipseJDT中的增量式編譯器(ECJ)。前端
JIT編譯器:HotSpot VM的C1/C2編譯器。java
AOT編譯器:GNU Compiler for the Java(GCJ)/Excelsior JET。程序員
Javac這類編譯器對代碼的運行效率幾乎沒有任何優化措施。虛擬機設計團隊把對性能的優化集中到了後端的即時編譯器中,可讓那些不是由Javac產生的Class文件也能享受到編譯器優化所帶來的好處。但Javac作了許多針對編碼過程的優化措施來改變程序員的編碼風格和提升編碼效率。至關多新生的Java愈發特性,都是靠編譯器的「語法糖」來實現,而不是依賴虛擬機的底層改進來支持。Java中即時編譯器在運行時期的優化過程對於程序運行更重要,而前端編譯器在編譯期的優化過程對於程序編碼關係更加密切。後端
Javac編譯器app
Javc編譯器是由Java編寫的程序。函數
Javac的源碼與調式性能
從Sun Javac的代碼看來,編譯過程大體能夠分爲三個過程:優化
解析與填充符號表過程。ui
插入式註解處理器的註解處理過程。
分析與字節碼生成過程。
Javac編譯動做的入口是com.sun.tools.javac.main.JavaCompiler類,上述三個過程的代碼邏輯集中在這個類的compile()和compile2()裏,整個編譯最關鍵的處理由8個方法完成。
解析與填充符號表
解析步驟由圖中的parseFiles()完成,解析步驟包括了經典程序編譯原理中的詞法分析和語法分析。
1 詞法/語法分析
詞法分析是將源代碼的字符流轉變爲標記(Token)集合,單個字符時程序編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵字/變量名/字面量和運算符均可以成爲標記。在Javac的源碼中,詞法分析由com.sun.tools.javac.parser.Scanner類實現。
語法分析是根據Token序列來構造抽象語法樹的過程,抽象語法樹(AST,Abstract Syntax Tree)是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每個節點都表明着程序代碼中的一個語法結構(Construct),例如包/類型/修飾符/運算符/接口/返回值甚至連代碼注視等均可以是一個語法結構。
在Javac源碼中,語法分析由com.sun.tools.javac.parser.Parser類實現,這個階段產生的抽象語法樹由ccom.sun.tools.javac.tree.JCTree類表示,通過這個步驟後,編譯器就基本不會再對源碼文件進行操做了,後續操做都創建在抽象語法樹上。
2 填充符號表
由enterTrees()完成。符號表(Symbol Table)由一組符號地址和符號信息構成的表格。符號表中登記的信息在編譯的不一樣階段都要用到。在語義分析中,符號表登記的內容將用於語義檢查(如檢查一個名字的使用和原先的說明是否一致)和產生中間代碼。在目標代碼生成階段,當對符號名進行地址分配時,符號表示地址分配的依據。
由com.sun.tools.javac.comp.Enter類實現,此過程的出口是一個待處理列表(To Do List),包含了每個編譯單元的抽象語法樹的頂級節點,以及package-info.java(若是存在的話)的頂級節點。
註解處理器
在JDK1.6中實現了JSR-269規範,提供了一組插入式註解處理器的標準API在編譯期對註解進行處理,咱們能夠把它看作是一組編譯器的插件,在這些插件裏面,能夠讀取/修改/添加抽象語法樹中的任意元素。若是這些插件在處理註解期間對語法樹進行了修改,那麼編譯器將回到解析及填充符號表的過程從新處理,直到全部的插入式註解處理器都沒有再對語法樹進行修改成止,每一次循環稱爲一個Round。
語義分析與字節碼生成
語法樹能表示一個結構正確的源程序的抽象,但沒法保證源程序是符合邏輯的。而語義分析的主要任務是對結構上正確的源程序進行上下文有關性質的審查,如進行類型審查。
1 標註檢查
Javac編譯過程,語義分析過程爲標註檢查和數據及控制流分析兩個步驟,分別由attribute()和flow()完成。
標註檢查步驟檢查的內容包括變量使用前是由已經聲明/變量與賦值之間的數據類型是否匹配等。還有一個重要的動做稱爲常量摺疊,若是咱們在代碼中:
int a = 1 + 2;
在語法樹上仍然看到字面量「1」/「2」和操做符「+」號,可是通過常量摺疊後,它們將會被摺疊爲字面量「3」。因爲編譯期間進行了常量摺疊,因此在代碼中「a=1+2」比起直接定義「a=3」,並不會增長程序運行期哪怕僅僅一個CPU指令的運算量。
2 數據及控制流分析
是對程序上下文邏輯更進一步的驗證,它能夠檢查出程序局部變量在使用前是否賦值/方法的每條路徑是否都有返回值/是否全部的受檢查異常都被正確處理等問題。編譯時期的數據及控制流分析與類加載時數據及控制流分析的目的基本上一致,但校驗範圍有所區別,有一些校驗項只有在編譯期或運行期才能進行。
//方法一帶有final修飾符
public void foo(final int arg){
final int var = 0;
}
//方法二沒有final修飾
public void foo(int arg){
int var = 0;
}
這兩段代碼編譯出來的Class文件沒有任何區別。將局部變量聲明爲final,對運行期沒有任何影響,變量的不變性由編譯器在編譯期間保障。
3. 解語法糖
語法糖(Syntactic Sugar),指在計算機語言中添加某種語法,這種語法對語言的功能並無影響,可是方便程序員使用。一般來講,使用語法糖能增長程序的可讀性,從而減小代碼出錯的機會。
Java中最經常使用的語法糖主要是泛型、變長參數、自動裝箱/拆箱等,虛擬機運行時不支持這些語法,它們在編譯階段還原回簡單的基礎語法結構,這個過程稱爲解語法糖。
4.字節碼生成
字節碼生成階段不只僅把前面各個步驟生成的信息(語法樹、符號表)轉換成字節碼寫到磁盤中,編譯器還進行了少許代碼添加和轉換工做。
例如,實例構造器<init>()方法和類構造器<cinit>()方法就是在這個階段添加到語法樹中(這裏的實例構造器不是指默認構造函數,若是代碼中沒有提供任何構造函數,那編譯器將會添加一個沒有參數的、訪問性(public、protected或private)與當前類一致的默認構造函數,這個工做在填充符號表階段已經完成),這兩個構造器的產生過程其實是一個代碼收斂的過程,編譯器會把語句塊(對於實例構造器是{}塊,對於類構造器是static塊)、變量初始化(實例變量和類變量)、調用父類的實例構造器等操做收斂到<init>()和<cinit>()中,並保證必定先執行父類的實例構造器,而後初始化變量,最後執行語句塊的順序進行。除了生成構造器外,還有其餘一些代碼替換工做用於優化程序的實現邏輯,如把字符串的加操做替換爲StringBuilder(大於等於JDK1.5)的append()操做。
Java語法糖的味道
泛型與類型擦除
泛型是JDK1.5新增特性,它的本質是參數化類型(Parametersized Type)的應用,也就是說所操做的數據類型被指定爲一個參數。這種參數類型能夠用在類、接口和方法的建立中,分別爲泛型類、泛型接口和泛型方法。
Java中泛型只在程序源碼中存在,在編譯後的字節碼文件中,就已經替換爲原生類型(Raw Type,也稱裸類型),並在相應的地方插入了強制轉型代碼,所以,對於運行期的Java來講,ArrayList<Integer>與ArrayList<String>就是同一類,因此泛型技術其實是Java的一顆語法糖,Java中泛型實現方法稱爲類型擦除,基於這種方法實現的泛型稱爲僞泛型。
//泛型擦除前 public static void main(String[] args) { Map<String, String> map = new HashMap<>(); map.put("java", "hello java"); map.put("jasvascript", "hello javascript"); System.out.println(map.get("java")); System.out.println(map.get("jasvascript")); } //泛型擦除後 public static void main(String[] args) { Map map = new HashMap(); map.put("java", "hello java"); map.put("jasvascript", "hello javascript"); System.out.println((String)map.get("java")); System.out.println((String)map.get("jasvascript")); }
自動裝箱、拆箱與遍歷循環
//自動裝箱、拆箱與遍歷循環 public static void main(String[] args) { List<Integer> list = Arrays.asList(1,2,3,4); int sum = 0; for(int i : list){ sum += i; } System.out.println(sum); } //自動裝箱、拆箱與遍歷循環編譯以後 public static void main(String[] args) { List<Integer> list = Arrays.asList(new Integer[]{ Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4),}); int sum = 0; for(Iterator localIterator = list.iterator(); localIterator.hasNext();){ int i = ((Integer)localIterator.next()).intValue(); sum += i; } System.out.println(sum); }
遍歷循環須要被遍歷的類實現Iterable接口。
條件編譯
Java能夠進行條件編譯,方法是使用條件爲常量的if語句。
//Java語言中的條件編譯 public static void main(String[] args) { if(true){ System.out.println("block 1"); }else{ System.out.println("clock 2"); } } //上段代碼在編譯階段被「運行」,生成一下代碼 public static void main(String[] args) { System.out.println("block 1"); }
只有使用條件爲常量的if語句才能達到上述效果,若是使用常量與其餘帶條件判斷能力的語句搭配,則可能在控制流分析中提示錯誤,被拒絕編譯。
public static void main(String[] args) { //編譯器將會提示「Unreachable code」,拒絕編譯 while(false){ System.out.println(""); } }