深入理解java虛擬機(jī)(八)-編譯期優(yōu)化

本文基于周志明的《深入理解java虛擬機(jī) JVM高級(jí)特性與最佳實(shí)踐》所寫。特此推薦。

列舉了這3類編譯過(guò)程中一些比較有代表性的編譯器

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

相當(dāng)多新生的Java語(yǔ)法特性,都是靠編譯器的“語(yǔ)法糖”來(lái)實(shí)現(xiàn),而不是依賴虛擬機(jī)的底層改進(jìn)來(lái)支持,前端編譯器在編譯期的優(yōu)化過(guò)程對(duì)于程序編碼來(lái)說(shuō)關(guān)系更加密切。

Javac編譯器

Javac的源碼存放在JDK_SRC_HOME/langtools/src/share/classes/com/sun/tools/javac中, 除了JDK自身的API外 ,就只引用了JDK_SRC_HOME/langtools/src/share/classes/com/sun/*里面的代碼 ,調(diào)試環(huán)境建立起來(lái)簡(jiǎn)單方便,因?yàn)榛旧喜恍枰幚硪蕾囮P(guān)系。

從Sun Javac的代碼來(lái)看,編譯過(guò)程大致可以分為3個(gè)過(guò)程,分別是:

  • 解析與填充符號(hào)表的過(guò)程。
  • 插入式注解處理器的注解處理過(guò)程。
  • 分析與字節(jié)碼生成過(guò)程。

這3個(gè)步驟之間的關(guān)系與交互順序如下圖

Javac的編譯過(guò)程

Javac編譯動(dòng)作的入口是com.sun.tools.javac.main.JavaCompiler類 ,上述3個(gè)過(guò)程的代碼邏輯集中在這個(gè)類的compile() 和compile2() 方法中,其中主體代碼如下圖所示,整個(gè)編譯最關(guān)鍵的處理就由圖中標(biāo)注的8個(gè)方法來(lái)完成,下面我們具體看一下這8個(gè)方法實(shí)現(xiàn)了什么功能。

Javac編譯過(guò)程的主體代碼

解析與填充符號(hào)表

解析步驟由圖10-5中的parseFiles()方法(圖10-5中的過(guò)程1.1 ) 完成,解析步驟包括了經(jīng)典程序編譯原理中的詞法分析和語(yǔ)法分析兩個(gè)過(guò)程。

詞法、語(yǔ)法分析

詞法分析是將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記(Token)集合,單個(gè)字符是程序編寫過(guò)程的最小元素,而標(biāo)記則是編譯過(guò)程的最小元素,關(guān)鍵字、變量名、字面量、運(yùn)算符都可以成為標(biāo)記,如“int a=b+2”這句代碼包含了6個(gè)標(biāo)記,分別是int、a、=、b、+、2 ,雖然關(guān)鍵字int由 3個(gè)字符構(gòu)成,但是它只是一個(gè)Token,不可再拆分。在Javac的源碼中,詞法分析過(guò)程由com.sun.tools.javac.parser.Scanner類來(lái)實(shí)現(xiàn)。

語(yǔ)法分析是根據(jù)Token序列構(gòu)造抽象語(yǔ)法樹的過(guò)程,抽象語(yǔ)法樹( Abstract Syntax Tree,AST ) 是一種用來(lái)描述程序代碼語(yǔ)法結(jié)構(gòu)的樹形表示方式,語(yǔ)法樹的每一個(gè)節(jié)點(diǎn)都代表著程序代碼中的一個(gè)語(yǔ)法結(jié)構(gòu)( Construct ) ,例如包、類型、修飾符、運(yùn)算符、接口、返回值甚至代碼注釋等都可以是一個(gè)語(yǔ)法結(jié)構(gòu)。

在Javac的源碼中,語(yǔ)法分析過(guò)程由 com.sun.tools.javac.parser.Parser類實(shí)現(xiàn),這個(gè)階段產(chǎn)出的抽象語(yǔ)法樹由com.sun.tools.javac.tree.JCTree類表示,經(jīng)過(guò)這個(gè)步驟之后,編譯器就基本不會(huì)再對(duì)源碼文件進(jìn)行操作了,后續(xù)的操作都建立在抽象語(yǔ)法樹之上。

填充符號(hào)表

完成了語(yǔ)法分析和此法分析后,下一步就是填充符號(hào)表的過(guò)程,也就是圖10-5中enterTrees()方法(圖10-5中的過(guò)程1.2)所做的事情。符號(hào)表(Symbol Table)是由一組符號(hào)地址和符號(hào)信息構(gòu)成的表格,讀者可以把它想象成哈希表中K-V值對(duì)的形式(實(shí)際上符號(hào)表不一定是哈希表實(shí)現(xiàn),可以是有序符號(hào)表、樹狀符號(hào)表、棧結(jié)構(gòu)符號(hào)表等)。符號(hào)表中所登記的信息在編譯的不同階段都要用到。在語(yǔ)義分析中,符號(hào)表所登記的內(nèi)容將用于語(yǔ)義檢查(如檢查一個(gè)名字的使用和原先的說(shuō)明是否一致)和產(chǎn)生中間代碼。在目標(biāo)生成階段,當(dāng)對(duì)符號(hào)名進(jìn)行地址分配時(shí),符號(hào)表是地址分配的依據(jù)。

在Javac源代碼中,填充符號(hào)表的過(guò)程由com.sun.tools.javac.comp.Enter類實(shí)現(xiàn),此過(guò)程的出口是一個(gè)待處理列表( To Do List ) ,包含了每一個(gè)編譯單元的抽象語(yǔ)法樹的頂級(jí)節(jié)點(diǎn), 以及package-info.java ( 如果存在的話)的頂級(jí)節(jié)點(diǎn)。

注解處理器

在JDK 1.5之后,Java語(yǔ)言提供了對(duì)注解(Annotation ) 的支持,這些注解與普通的Java代碼一樣,是在運(yùn)行期間發(fā)揮作用的。在JDK 1.6中實(shí)現(xiàn)了JSR-269規(guī)范 ,提供了一組插入式注解處理器的標(biāo)準(zhǔn)API在編譯期間對(duì)注解進(jìn)行處理,我們可以把它看做是一組編譯器的插件 ,在這些插件里面,可以讀取、修改、添加抽象語(yǔ)法樹中的任意元素。如果這些插件在處理注解期間對(duì)語(yǔ)法樹進(jìn)行了修改,編譯器將回到解析及填充符號(hào)表的過(guò)程重新處理,直到所有插入式注解處理器都沒(méi)有再對(duì)語(yǔ)法樹進(jìn)行修改為止,每一次循環(huán)稱為一個(gè)Round,也就是圖10-4中的回環(huán)過(guò)程。

有了編譯器注解處理的標(biāo)準(zhǔn)API后 ,我們的代碼才有可能干涉編譯器的行為,由于語(yǔ)法樹中的任意元素,甚至包括代碼注釋都可以在插件之中訪問(wèn)到,所以通過(guò)插入式注解處理器實(shí)現(xiàn)的插件在功能上有很大的發(fā)揮空間。只要有足夠的創(chuàng)意,程序員可以使用插入式注解處理器來(lái)實(shí)現(xiàn)許多原本只能在編碼中完成的事情。

在Javac源碼中,插入式注解處理器的初始化過(guò)程是在initPorcessAnnotations() 方法中完成的,而它的執(zhí)行過(guò)程則是在processAnnotations() 方法中完成的,這個(gè)方法判斷是否還有新的注解處理器需要執(zhí)行,如果有的話,通過(guò)com.sun.tools.javac.processing.JavacProcessingEnvironment類的doProcessing() 方法生成一個(gè)新的JavaCompiler對(duì)象對(duì)編譯的后續(xù)步驟進(jìn)行處理。

語(yǔ)義分析與字節(jié)碼生成

語(yǔ)義分析的主要任務(wù)是對(duì)結(jié)構(gòu)上正確的源程序(抽象語(yǔ)法樹)進(jìn)行上下文有關(guān)性質(zhì)的審查,如進(jìn)行類型審查。

標(biāo)注檢查

Javac的編譯過(guò)程中,語(yǔ)義分析過(guò)程分為標(biāo)注檢查以及數(shù)據(jù)及控制流分析兩個(gè)步驟。分別由圖10-5中所示的attribute() 和flow() 方法(分別對(duì)應(yīng)圖10-5中的過(guò)程3.1和過(guò)程3.2) 完成。

標(biāo)注檢查步驟檢查的內(nèi)容包括諸如變量使用前是否已被聲明、變量與賦值之間的數(shù)據(jù)類型是否能夠匹配等。在標(biāo)注檢查步驟中,還有一個(gè)重要的動(dòng)作稱為常量折疊,如果我們?cè)诖a中寫了如下定義:

int a=1+2;

那么在語(yǔ)法樹上仍然能看到字面量“ 1”、“2”以及操作符“+”,但是在經(jīng)過(guò)常量折疊之后 ,它們將會(huì)被折疊為字面量“3” ,這個(gè)插入式表達(dá)式( Mix Expression )的值已經(jīng)在語(yǔ)法樹上標(biāo)注出來(lái)了(ConstantExpressionValue : 3 ) 。 由于編譯期間進(jìn)行了常量折疊 ,所以在代碼里面定義“a=1+2”比起直接定義“a=3” , 并不會(huì)增加程序運(yùn)行期哪怕僅僅一個(gè) CPU指令的運(yùn)算量。

標(biāo)注檢查步驟在Javac源碼中的實(shí)現(xiàn)類是com.sun.tools.javac.comp.Attr類和
com.sun.tools.javac.comp.Check類。

數(shù)據(jù)及控制流分析

數(shù)據(jù)及控制流分析是對(duì)程序上下文邏輯更進(jìn)一步的驗(yàn)證,它可以檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問(wèn)題。

在Javac的源碼中,數(shù)據(jù)及控制流分析的入口是圖 10-5中的flow() 方法(對(duì)應(yīng)圖10-5中的過(guò)程3.2) ,具體操作由com.sun.tools.javac.comp.Flow類來(lái)完成。

解語(yǔ)法糖

語(yǔ)法糖( Syntactic Sugar ) ,指在計(jì)算機(jī)語(yǔ)言中添加的某種語(yǔ)法,這種語(yǔ)法對(duì)語(yǔ)言的功能并沒(méi)有影響,但是更方便程序員使用。

Java中最常用的語(yǔ)法糖主要是前面提到過(guò)的泛型(泛型并不一定都是語(yǔ)法糖實(shí)現(xiàn),如C#的泛型就是直接由CLR支持的 )、變長(zhǎng)參數(shù)、自動(dòng)裝箱/拆箱等,虛擬機(jī)運(yùn)行時(shí)不支持這些語(yǔ)法 ,它們?cè)诰幾g階段還原回簡(jiǎn)單的基礎(chǔ)語(yǔ)法結(jié)構(gòu),這個(gè)過(guò)程稱為解語(yǔ)法糖。

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

字節(jié)碼生成

字節(jié)碼生成是Javac編譯過(guò)程的最后一個(gè)階段,在Javac源碼里面由com.sun.tools.javac.jvm.Gen類來(lái)完成。字節(jié)碼生成階段不僅僅是把前面各個(gè)步驟所生成的信息 (語(yǔ)法樹、符號(hào)表)轉(zhuǎn)化成字節(jié)碼寫到磁盤中,編譯器還進(jìn)行了少量的代碼添加和轉(zhuǎn)換工作。

完成了對(duì)語(yǔ)法樹的遍歷和調(diào)整之后,就會(huì)把填充了所有所需信息的符號(hào)表交給 com.sun.tools.javac.jvm.ClassWriter類 ,由這個(gè)類的writeClass()方法輸出字節(jié)碼,生成最終的Class文件 ,到此為止整個(gè)編譯過(guò)程宣告結(jié)束。

Java語(yǔ)法糖的味道

泛型與類型擦除

泛型是JDK 1.5的一項(xiàng)新增特性,它的本質(zhì)是參數(shù)化類型( Parametersized Type )的應(yīng)用 ,也就是說(shuō)所操作的數(shù)據(jù)類型被指定為一個(gè)參數(shù)。這種參數(shù)類型可以用在類、接口和方法的創(chuàng)建中,分別稱為泛型類、泛型接口和泛型方法。

java語(yǔ)言中的泛型只在程序源碼中存在,在編譯后的字節(jié)碼文件中,就已經(jīng)替換為原來(lái)的原生類型( Raw Type,也稱為裸類型 )了,并且在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)型代碼,因此,對(duì)于運(yùn)行期的Java語(yǔ)言來(lái)說(shuō),ArrayList<int>與ArrayList<String>就是同一個(gè)類,所以泛型技術(shù)實(shí)際上是Java語(yǔ)言的一顆語(yǔ)法糖,Java語(yǔ)言中的泛型實(shí)現(xiàn)方法稱為類型擦除 ,基于這種方法實(shí)現(xiàn)的泛型稱為偽泛型。

由于Java泛型的引入,各種場(chǎng)景(虛擬機(jī)解析、反射等)下的方法調(diào)用都可能對(duì)原有的基礎(chǔ)產(chǎn)生影響和新的需求,如在泛型類中如何獲取傳入的參數(shù)化類型等。因此 ,JCP組織對(duì)虛擬機(jī)規(guī)范做出了相應(yīng)的修改,引入了諸如Signature、LocalVariableTypeTable等新的屬性用于解決伴隨泛型而來(lái)的參數(shù)類型的識(shí)別問(wèn)題,Signature是其中最重要的一項(xiàng)屬性,它的作用就是存儲(chǔ)一個(gè)方法在字節(jié)碼層面的特征簽名,這個(gè)屬性中保存的參數(shù)類型并不是原生類型 ,而是包括了參數(shù)化類型的信息。修改后的虛擬機(jī)規(guī)范要求所有能識(shí)別49.0以上版本的 Class文件的虛擬機(jī)都要能正確地識(shí)別Signature參數(shù)。

另外 ,從Signature屬性的出現(xiàn)我們還可以得出結(jié)論,擦除法所謂的擦除,僅僅是對(duì)方法的Code屬性中的字節(jié)碼進(jìn)行擦除,實(shí)際上元數(shù)據(jù)中還是保留了泛型信息,這也是我們能通過(guò)反射手段取得參數(shù)化類型的根本依據(jù)。

自動(dòng)裝箱、拆箱與遍歷循環(huán)

泛型就不必說(shuō)了,自動(dòng)裝箱、拆箱在編譯之后被轉(zhuǎn)化成了對(duì)應(yīng)的包裝和還原方法,如本例中的Integer.valueOf() 與Integer.intValue() 方法,而遍歷循環(huán)則把代碼還原成了迭代器的實(shí)現(xiàn),這也是為何遍歷循環(huán)需要被遍歷的類實(shí)現(xiàn)Iterable接口的原因。最后再看看變長(zhǎng)參數(shù),它在調(diào)用的時(shí)候變成了一個(gè)數(shù)組類型的參數(shù),在變長(zhǎng)參數(shù)出現(xiàn)之前,程序員就是使用數(shù)組來(lái)完成類似功能的。

條件編譯

Java語(yǔ)言當(dāng)然也可以進(jìn)行條件編譯,方法就是使用條件為常量的if語(yǔ)句。只能使用條件為常量的if語(yǔ)句才能達(dá)到效果,如果使用常量與其他帶有條件判斷能力的語(yǔ)句搭配,則可能在控制流分析中提示錯(cuò)誤,被拒絕編譯。

Java語(yǔ)言中條件編譯的實(shí)現(xiàn),也是Java語(yǔ)言的一顆語(yǔ)法糖,根據(jù)布爾常量值的真假,編譯器將會(huì)把分支中不成立的代碼消除掉 ,這一工作將在編譯器解除語(yǔ)法糖階段
( com.sun.tools.javac.comp.Lower類中)完成。由于這種條件編譯的實(shí)現(xiàn)方式使用了if語(yǔ)句,所以它必須遵循最基本的Java語(yǔ)法 ,只能寫在方法體內(nèi)部,因此它只能實(shí)現(xiàn)語(yǔ)句基本塊 ( Block)級(jí)別的條件編譯,而沒(méi)有辦法實(shí)現(xiàn)根據(jù)條件調(diào)整整個(gè)Java類的結(jié)構(gòu)。

除了介紹的泛型、自動(dòng)裝箱、自動(dòng)拆箱、遍歷循環(huán)、變長(zhǎng)參數(shù)和條件編譯之外 ,Java語(yǔ)言還有不少其他的語(yǔ)法糖,如內(nèi)部類、枚舉類、斷言語(yǔ)句、對(duì)枚舉和字符串(在 JDK 1.7中支持)的switch支持、try語(yǔ)句中定義和關(guān)閉資源(在JDK 1.7中支持)等 。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容