1. 程式人生 > >《深入理解Java虛擬機器——JVM高階特性與最佳實踐》學習筆記——程式編譯與程式碼優化(一)

《深入理解Java虛擬機器——JVM高階特性與最佳實踐》學習筆記——程式編譯與程式碼優化(一)

早期(編譯期)優化

Javac的編譯過程

  • 解析與填充符號表過程
  • 插入式註解處理器的註解處理過程
  • 分析與位元組碼生成過程

<img 9001>

Javac編譯動作的入口是com.sun.tools.javac.main.JavaCompiler類,上述3個過程的程式碼邏輯集中在這個類的compile()和compile2()方法中,其中主體程式碼如下圖所示,整個編譯最關鍵的處理就由圖中標註的8個方法來完成

<img 9002>

解析與填充符號表

解析步驟由parseFiles()方法完成,解析步驟包括了經典程式編譯原理中的詞法分析和語法分析兩個過程

  • 詞法、語法分析

    詞法分析是將原始碼的字元流轉變為標記(Token)集合,單個字元是程式編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵字、變數名、字面量、運算子都可以成為標記,如”int a=b+2”這句程式碼中包含了6個標記,分別是int、a、=、b、+、2。在Javac的原始碼中,詞法分析過程由com.sun.tools.javac.parser.Scanner類來實現

    語法分析是根據Token序列構造抽象語法樹的過程,抽象語法樹(AST)是一種用來描述程式程式碼語法結構的樹形表示方式,語法樹的每一個節點都代表著程式程式碼中的一個語法結構,例如包、型別、修飾符、運算子、介面、返回值甚至程式碼註釋等都可以是一個語法結構。語法分析過程由com.sun.tools.javac.parser.Parser類實現,這個階段產生的抽象語法樹由com.sun.tools.javac.tree.JCTree類表示,經過這個步驟之後,編譯器就基本不會再對原始碼檔案進行操作了,後續的操作都建立在抽象語法樹之上

  • 填充符號表

    完成了語法分析和詞法分析之後,下一步就是填充符號表的過程,也就是enterTrees()方法所做的事情。符號表(System Table)是由一組符號地址和符號資訊構成的表格,符號表中所登記的資訊在編譯的不同階段都要用到。在語義分析中,符號表所登記的內容將用於語義檢查(如檢查一個名字的使用和原先的說明是否一致)和產生中間程式碼,在目的碼生成階段,當對符號名進行地址分配時,符號表是地址分配的依據

    在Javac原始碼中,填充符號表由com.sun.tools.javac.comp.Enter類實現,此過程的出口是一個待處理列表,包含了每一個編譯單元的抽象語法樹的頂級節點,以及package-info.java(如果存在的話)的頂級節點

註解處理器

註解與普通Java程式碼一樣,是在執行期間發揮作用的,在JDK 1.6中實現了JSR-269規範,提供了一組插入式註解處理器的標準API在編譯期間對註解進行處理,可以把它看做是一組編譯器的外掛,在這些外掛裡面,可以讀取、修改、新增抽象語法樹的任意元素

在Javac原始碼中,插入式註解處理器的初始化過程是在initPorcessAnnotations()方法中完成的,而它的執行過程則是在processAnnotations()方法中完成的,這個方法判斷是否還有新的註解處理器需要執行,如果有的話,通過com.sun.tools.javac.processing.JavacProcessingEnvironment類的doProcessing()方法生成一個新的JavaCompiler物件對編譯的後續步驟進行處理

語義分析與位元組碼生成

語法分析之後,編譯器獲得了程式程式碼的抽象語法樹表示,語法樹能表示一個結構正確的源程式的抽象,但無法保證源程式是符合邏輯的,而語義分析的主要任務是對結構上正確的源程式進行上下文有關性質的審查,如進行型別審查

Javac的編譯過程中,語義分析過程分為標註檢查以及資料及控制流分析兩個步驟,分別對應attribute()和flow()方法

  • 標註檢查

    標註檢查步驟檢查的內容包括諸如變數使用前是否已被宣告、變數與賦值之間的資料型別是否能夠匹配等。在標註檢查步驟中,還有一個重要的動作稱為常量摺疊,如定義:

    int a = 1 + 2
    

    那麼在語法樹上仍然能看到字面量”1”、”2”以及操作符”+”,但是經過常量摺疊之後,它們將會被摺疊為字面量”3”。由於編譯期間進行了常量摺疊,所以在程式碼中的定義”a=1+2”比起直接定義”a=3”,並不會增加程式執行期哪怕僅僅一個CPU指令的運算量

    標註檢查步驟在Javac原始碼中的實現類是com.sun.tools.javac.comp.Attr類和com.sun.tools.javac.comp.Check類

  • 資料及控制流分析

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

    在Javac的原始碼中,資料及控制流分析的入口是flow()方法,具體操作由com.sun.tools.javac.comp.Flow類來完成

  • 解語法糖

    語法糖(Syntactic Sugar),也稱糖衣語法,指在計算機語言中新增的某種語法,使用語法糖能夠增加程式的可讀性,從而減少程式碼出錯的機會

    Java中最常用的語法糖主要是泛型、變長引數、自動裝箱/拆箱等,虛擬機器執行時不支援這些語法,它們在編譯階段還原回簡單的基礎語法結構,這個過程稱為解語法糖

    在啊Javac的原始碼中,解語法糖的過程由desugar()方法觸發,在com.sun.tools.javac.comp.TransTypes類和com.sun.tools.javac.comp.Lower類中完成

  • 位元組碼生成

    位元組碼生成是Javac編譯過程的最後一個階段,在Javac原始碼裡面由com.sun.tools.javac.jvm.Gen類來完成,位元組碼生成階段不僅僅是把前面各個步驟所生成的資訊(語法樹、符號表)轉化成位元組碼寫到磁碟中,編譯器還進行了少量的程式碼新增和轉換工作

    完成了對語法樹的遍歷和調整之後,就會把填充了所有所需資訊的符號表交給com.sun.tools.javac.jvm.ClassWriter類,由這個類的writeClass()方法輸出位元組碼,生成最終的Class檔案,到此整個編譯過程宣告結束

Java語法糖

泛型與型別擦除

泛型是JDK 1.5的一項新增特性,它的本質是引數化型別的應用,也就是說所操作的資料型別被指定為一個引數,這種引數型別可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面和泛型方法

Java語言的泛型只在程式原始碼中存在,在編譯後的位元組碼檔案中,就已經替換為原來的原生型別(Raw Type,也稱為裸型別)了,並且在相應的地方插入了強制轉型程式碼,因此,對於執行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類,所以泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱為型別擦除,基於這種方法實現的泛型稱為偽泛型

//泛型擦除前
Map<String,String> map = HashMap<String,String>();
map.put("hello","你好");
System.out.println(map.get("hello"));

//泛型擦除後
Map map = new HashMap();
map.put("hello","你好");
System.out.println((String)map.get("hello"));

自動裝箱、拆箱與遍歷迴圈

自動裝箱、拆箱在編譯之後被轉化成了對應的包裝和還原方法,如Integer.valueOf()與Integer.intValue()方法

變長引數在呼叫的時候變成了一個數組型別的引數

條件編譯

Java中的if語句在編譯階段就會被”執行”,根據布林常量值的真假,編譯器會把分支中不成立的程式碼塊消除掉,這一工作將在編譯器解除語法糖階段(com.sun.tools.javac.comp.Lower類中)完成