《深刻理解 Java 虛擬機》讀書筆記:早期(編譯期)優化

正文

Java 語言的 3 類編譯器:前端

  • 前端編譯器:把 java 文件 轉變成 class 文件。例如:Sun 的 Javac。
  • JIT 編譯器(即時編譯器):後端運行期編譯器,把字節碼轉變成機器碼。例如:HotSpot VM 的 C一、C2 編譯器。
  • AOT 編譯器(靜態提早編譯器):直接把 java 文件編譯成本地機器代碼。例如:GCJ(GNU Compiler for the Java)。

Java 語言的「編譯期」是一段「不肯定」的操做過程,它多是 3 類編譯器中某類的編譯過程。java

1、Javac 編譯器

Javac 的編譯過程大體可分爲 3 個過程:程序員

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

這 3 個步驟之間的關係與交互順序以下圖所示:後端

Javac 的編譯動做入口是 com.sun.tools.javac.main.JavaCompiler 類,上述 3 個過程的代碼邏輯集中在這個類的 compile() 和 compile2() 方法中。數組

一、解析與填充符號表

(1)詞法、語法分析

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

語法分析是根據 Token 序列構造抽象語法樹的過程。插件

抽象語法樹(AST)是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每個節點都表明着程序代碼中的一個語法結構,例如包、類型、修飾符、運算符、接口、返回值甚至代碼註釋等均可以是一個語法結構。生成抽象語法樹以後,編譯器的後續操做基本都創建在抽象語法樹之上。code

在 Javac 源碼中,詞法分析過程由 com.sun.tools.javac.parser.Scanner 類實現,語法分析過程則由 com.sun.tools.javac.parser.Parser 類實現,生成的抽象語法樹由 com.sun.tools.javac.tree.JCTree 類表示。blog

(2)填充符號表

符號表是由一組符號地址和符號信息構成的表格,能夠把它想象成哈希表中 K-V 值對的形式。符號表中所登記的信息在編譯的不一樣階段都要用到。接口

在 Javac 源碼中,填充符號表的過程由 com.sun.tools.javac.comp.Enter 類實現。

二、註解處理

JDK1.6 提供了一組插入式註解處理器的標準 API,在編譯期間對註解進行處理。能夠把這組 API 看作是一組編譯器的插件,在這些插件裏面,能夠讀取、修改、添加抽象語法樹中的任何元素。

若是這些插件在處理註解期間對語法樹進行了修改,編譯器將回到解析及填充符號表的過程從新處理,直到全部插入式註解處理器都沒有再對語法樹進行修改成止。

在 Javac 源碼中,插入式註解處理的初始化過程是在 initProcessAnnotations() 方法中完成的,它的執行過程則是在 processAnnotations() 方法中完成的。

三、語義分析與字節碼生成

語法分析以後,編譯器得到了程序代碼的抽象語法樹表示,語法樹能表示一個結構正確的源程序的抽象,但沒法保證源程序是符合邏輯的。

語義分析的主要任務就是對結構上正確的源程序進行上下文邏輯審查,包括標註檢查、數據及控制流分析兩個步驟。

(1)標註檢查

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

常量摺疊:
標註檢查會將表達式的結果值在語法樹上標註出來,這個動做稱爲常量摺疊。
好比定義了一個 int a = 1 + 2;。在語法樹上仍然能看到字面量「1」、「2」以及操做符「+」,可是通過常量摺疊後,會被摺疊爲字面量「3」,而且會在語法樹上標註出來。

在 Javac 源碼中,標註檢查的入口是 attribute() 方法,具體操做由 com.sun.tools.javac.comp.Attr 類和 com.sun.tools.javac.comp.Check類完成。

(2)數據及控制流分析

數據及控制流分析是對程序上下文邏輯更進一步的驗證,包括局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否全部的受查異常都被正確處理等。

編譯期的數據及控制流分析與類加載時的數據及控制流分析的目的基本上是一致的,只是檢驗範圍有所區別,有一些校驗項只有在編譯期或運行期才能進行。

在 Javac 源碼中,數據及控制流分析的入口是 flow() 方法,具體操做由 com.sun.tools.javac.comp.Flow類完成。

(3)解語法糖

語法糖是指在計算機語言中添加的某種語法,這種語法對語言的功能沒有影響,可是可以方便程序員使用,增長程序的可讀性,從而減小程序代碼出錯的機會。

虛擬機運行時並不支持語法糖的語法,所以,須要在編譯階段還原回簡單的基礎語法結構,這個過程稱爲解語法糖。

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

(4)字節碼生成

字節碼生成是 Javac 編譯過程的最後一個階段,在 Javac 源碼中由 com.sun.tools.javac.jvm.Gen 類完成。

字節碼生成階段不只僅是把前面各個步驟所生成的信息(語法樹、符號表)轉化成字節碼寫到磁盤中,編譯器還進行了少許的代碼添加和轉換工做。例如,實例構造器 init() 方法和類構造器 clinit() 方法就是在這個階段添加到語法樹之中的。

完成了對語法樹的遍歷和調整以後,會把填充了全部所需信息的符號表交給 com.sun.tools.javac.jvm.ClassWriter 類,由這個類的 writeClass() 方法輸出字節碼,生成最終的 class 文件。

2、Java 語法糖的味道

一、泛型與類型擦除

泛型的本質是參數化類型的應用,即將所操做的數據類型指定爲一個參數。這種參數類型能夠用在類、接口和方法的建立中,分別稱爲泛型類、泛型接口和泛型方法。

C# 與 Java 的泛型:
泛型技術在 C# 和 Java 之中的使用方式看似相同,但實現上卻有着根本性的分歧。

C# 的泛型不管在程序源碼中、編譯後的 IL (中間語言,這時候泛型是一個佔位符)中,或是運行期的 CLR(公共語言運行庫) 中,都是切實存在的。在 C# 中,List 與 List 就是兩個不一樣的類型,它們在系統運行期生成,有本身的虛方法表和數據類型,這種實現稱爲 類型膨脹,基於這種方法實現的泛型稱爲 真實泛型

Java 的泛型只在程序源碼中存在,在編譯後的字節碼文件中,就已經替換爲原來的原生類型(也稱爲裸類型)了,而且在相應的地方插入了強制轉型代碼。所以,對於運行期的 Java 語言來講,ArrayList 與 ArrayList 就是同一個類,因此泛型技術其實是 Java 語言的一顆語法糖,Java 語言中的泛型實現方法稱爲 類型擦除,基於這種方法實現的泛型稱爲 僞泛型

二、自動裝箱與拆箱、遍歷循環、變長參數

自動裝箱與拆箱在編譯以後被轉化成了對應的包裝和還原方法。好比 Integer.valueOf() 與 Integer.intValue()。

遍歷循環把代碼還原成了迭代器的實現,這也是爲什麼遍歷循環須要被遍歷的類實現 Iterable 接口的緣由。

變長參數在調用的時候變成了一個數組類型的參數。

三、條件編譯

條件編譯是指編譯器在編譯時只對知足條件的代碼進行編譯,而將不知足條件的代碼捨棄。

Java 語言可使用條件爲布爾常量值的 if 語句進行條件編譯。根據布爾常量值的真假,編譯器會捨棄分支中不成立的代碼塊。

相關文章
相關標籤/搜索