java程序编译_Java程序的编译过程
Java的編譯期是一個(gè)模糊的概念,需要具體分析。
將 *.java文件轉(zhuǎn)為 *.class的過程稱為編譯器的前端(前端編譯)。例如:JDK的javac編譯器。
把字節(jié)碼( *.class文件) 轉(zhuǎn)變?yōu)?本地機(jī)器碼 的過程稱為Java虛擬機(jī)的即時(shí)編譯運(yùn)行期(JIT編譯器,Just In Time)。例如:HotSpot虛擬機(jī)的C1、C2編譯器。
使用靜態(tài)的提前編譯器(AOT編譯器,Ahead Of Time Compiler)直接把程序變異成與目標(biāo)及其指令集相關(guān)的二進(jìn)制代碼的過程。例如:JDK的Jaotc。
一.前端編譯與優(yōu)化
Javac 這類編譯器對代碼的運(yùn)行效率幾乎沒有任何優(yōu)化措施,虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)把對性能的優(yōu)化都放到了后端的即時(shí)編譯器中,這樣可以讓那些不是由 Javac 產(chǎn)生的 class 文件(如 Groovy、Kotlin 等語言產(chǎn)生的 class 文件)也能享受到編譯器優(yōu)化帶來的好處。但是 Javac 做了很多針對 Java 語言編碼過程的優(yōu)化措施來改善程序員的編碼風(fēng)格、提升編碼效率。相當(dāng)多新生的 Java 語法特性,都是靠編譯器的「語法糖」來實(shí)現(xiàn)的,而不是依賴虛擬機(jī)的底層改進(jìn)來支持。
Java 中即時(shí)編譯器在運(yùn)行期的優(yōu)化過程對于程序運(yùn)行來說更重要,而前端編譯器在編譯期的優(yōu)化過程對于程序編碼來說更加密切。
1.1 Javac 編譯器
Javac 編譯器的編譯過程大致可分為 1個(gè)準(zhǔn)備過程3個(gè)處理過程 :
初始化插入式注解處理器
解析與填充符號表;
插入式注解處理器的注解處理;
分析與字節(jié)碼生成。
這 3 個(gè)步驟之間的關(guān)系如下圖所示:
解析與填充符號表
🌳 解析步驟包含了經(jīng)典程序編譯原理中的詞法分析和語法分析兩個(gè)過程:
詞法分析是將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記(Token)集合的過程,單個(gè)字符是程序?qū)憰r(shí)的最小元素,但標(biāo)記才是編譯時(shí)的最小元素。關(guān)鍵字、變量名、字面量、運(yùn)算符都可以作為標(biāo)記,如“inta=b+2”這句代碼中就包含了6個(gè)標(biāo)記,分別是imt、a、=、b、+、1雖然關(guān)鍵字int由3個(gè)字符構(gòu)成,但是它只是一個(gè)獨(dú)立的標(biāo)記,不可以再拆分。
語法分析是根據(jù)標(biāo)記序列構(gòu)造抽象語法樹的過程,抽象語法樹是一種用來描述程序代碼語法結(jié)構(gòu)的樹形表示方式,抽象語法樹的每一個(gè)節(jié)點(diǎn)都代表者程序代碼中的一個(gè)語法結(jié)構(gòu)。例如包、類型、修飾符、運(yùn)算符、接口返回值甚至連代碼注釋等都可以是一種特定的語法結(jié)構(gòu)。
🌳 填充符號表
完成詞法分析和語法分析之后,下一步就是填充符號表的過程。符號表是由一組符號地址和符號信息構(gòu)成的表格。在語義分析中,符號表所登記的內(nèi)容將用于語義檢查和產(chǎn)生中間代碼。在目標(biāo)代碼生成階段,當(dāng)對符號名進(jìn)行地址分配時(shí),符號表是地址分配的依據(jù)。
注解處理器
注解(Annotation)是在 JDK 1.5 中新增的,注解在設(shè)計(jì)上原本是與普通代碼一樣,只在運(yùn)行期間發(fā)揮作用。
但是在JDK1.6中,插入式注解處理器可以提前至編譯期對代碼中的特點(diǎn)注解進(jìn)行處理,從而影響到前端編譯器的工作過程。我們可以把插入式注解處理器看作是一組編譯器的插件,當(dāng)這些插件工作時(shí),允許讀取、修改、添加抽象語法樹中的任意元素。如果這些插件在處理注解期間對語法樹進(jìn)行過修改,編譯器將回到解析及填充符號表的過程重新處理,直到所有插入式注解處理器都沒有再對語法樹進(jìn)行修改為止,每一次循環(huán)過程稱為一個(gè)輪次(Round),這也就對應(yīng)著?? 圖的那個(gè)回環(huán)過程有了編譯器注解處理過程。Lombok就是依賴于插入式注解器實(shí)現(xiàn)的。
語義分析與字節(jié)碼生成
語法分析之后,編譯器獲得了程序代碼的抽象語法樹表示,語法樹能表示一個(gè)結(jié)構(gòu)正確的源程序的抽象,但無法保證源程序是符合邏輯的。而語義分析的主要任務(wù)是對結(jié)構(gòu)上正確的源程序進(jìn)行上下文有關(guān)性質(zhì)的審查,比如進(jìn)行類型檢查,控制流檢查,數(shù)據(jù)流檢查,解語發(fā)糖。
字節(jié)碼生成是 Javac 編譯過程的最后一個(gè)階段,字節(jié)碼生成階段不僅僅是把前面各個(gè)步驟所生成的信息(語法樹、符號表)轉(zhuǎn)化成字節(jié)碼寫到磁盤中,編譯器還進(jìn)行了少量的代碼添加和轉(zhuǎn)換工作。如前面提到的 () 方法和()方法 就是在這一階段添加到語法樹中的。
在字節(jié)碼生成階段,除了生成構(gòu)造器以外,還有一些其它的代碼替換工作用于優(yōu)化程序的實(shí)現(xiàn)邏輯,如把字符串的加操作替換為 StringBiulder 或 StringBuffer。
完成了對語法樹的遍歷和調(diào)整之后,就會把填充了所需信息的符號表交給 com.sun.tools.javac.jvm.ClassWriter 類,由這個(gè)類的 writeClass() 方法輸出字節(jié)碼,最終生成字節(jié)碼文件,到此為止整個(gè)編譯過程就結(jié)束了。
1.2 Java 語法糖
Java 中提供了有很多語法糖來方便程序開發(fā),雖然語法糖不會提供實(shí)質(zhì)性的功能改進(jìn),但是它能提升開發(fā)效率、語法的嚴(yán)謹(jǐn)性、減少編碼出錯(cuò)的機(jī)會。下面我們來了解下語法糖背后我們看不見的東西。
泛型與類型擦除
泛型顧名思義就是類型泛化,本質(zhì)是參數(shù)化類型的應(yīng)用,也就是說操作的數(shù)據(jù)類型被指定為一個(gè)參數(shù)。這種參數(shù)可以用在類、接口和方法的創(chuàng)建中,分別稱為泛型類、泛型接口和泛型方法。
在 Java 語言還沒有泛型的時(shí)候,只能通過 Object 是所有類型的父類和強(qiáng)制類型轉(zhuǎn)換兩個(gè)特點(diǎn)的配合來實(shí)現(xiàn)類型泛化。例如 HashMap 的 get() 方法返回的就是一個(gè) Object 對象,那么只有程序員和運(yùn)行期的虛擬機(jī)才知道這個(gè) Object 到底是個(gè)什么類型的對象。在編譯期間,編譯器無法檢查這個(gè) Object 的強(qiáng)制類型轉(zhuǎn)換是否成功,如果僅僅依賴程序員去保障這項(xiàng)操作的正確性,許多 ClassCastException 的風(fēng)險(xiǎn)就會轉(zhuǎn)嫁到程序運(yùn)行期。
Java 語言中泛型只在程序源碼中存在,在編譯后的字節(jié)碼文件中,就已經(jīng)替換為原來的原生類型,并且在相應(yīng)的地方插入了強(qiáng)制類型轉(zhuǎn)換的代碼。因此對于運(yùn)行期的 Java 語言來說, ArrayList 與 ArrayList 是同一個(gè)類型,所以泛型實(shí)際上是 Java 語言的一個(gè)語法糖,這種泛型的實(shí)現(xiàn)方法稱為類型擦除。
自動(dòng)裝箱、拆箱與遍歷循環(huán)
自動(dòng)裝箱、拆箱與遍歷循環(huán)是 Java 語言中用得最多的語法糖。這塊比較簡單,我們直接看代碼:
public class SyntaxSugars {
public static void main(String[] args){
List list = Arrays.asList(1,2,3,4,5);
int sum = 0;
for(int i : list){
sum += i;
}
System.out.println("sum = " + sum);
}
}
自動(dòng)裝箱、拆箱與遍歷循環(huán)編譯之后:
public class SyntaxSugars {
public static void main(String[] args) {
List list = Arrays.asList(new Integer[]{
Integer.valueOf(1),
Integer.valueOf(2),
Integer.valueOf(3),
Integer.valueOf(4),
Integer.valueOf(5)
});
int sum = 0;
for (Iterator iterable = list.iterator(); iterable.hasNext(); ) {
int i = ((Integer) iterable.next()).intValue();
sum += i;
}
System.out.println("sum = " + sum);
}
}
第一段代碼包含了泛型、自動(dòng)裝箱、自動(dòng)拆箱、遍歷循環(huán)和變長參數(shù) 5 種語法糖,第二段代碼則展示了它們在編譯后的變化。
條件編譯
Java 語言中條件編譯的實(shí)現(xiàn)也是一顆語法糖,根據(jù)布爾常量值的真假,編譯器會把分支中不成立的代碼塊消除。
public static void main(String[] args) {
if (true) {
System.out.println("block 1");
} else {
System.out.println("block 2");
}
}
上述代碼經(jīng)過編譯后 class 文件的反編譯結(jié)果:
public static void main(String[] args) {
System.out.println("block 1");
}
二. 后端編譯與優(yōu)化
目前主流的兩款商用Java虛擬機(jī)(Hotspot、Open9)里,Java程序最初都是通過解釋器(Interpreter)進(jìn)行解釋執(zhí)行的。在javac編譯過后產(chǎn)生的字節(jié)碼Class文件:源碼在編譯的過程中,進(jìn)行「詞法分析 → 語法分析 → 生成目標(biāo)代碼」等過程,完成生成字節(jié)碼文件的工作。然后在后面交由解釋器)解釋執(zhí)行,省去前面預(yù)編譯的開銷。java.exe可以簡單看成是Java解釋器。
2.1 HotSpot 虛擬機(jī)內(nèi)的即時(shí)編譯器
當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或者代碼塊的運(yùn)行特別頻繁時(shí),就會把這些代碼認(rèn)定為「熱點(diǎn)代碼」(Hot Spot Code)。為了提高熱點(diǎn)代碼的執(zhí)行效率,在運(yùn)行時(shí),虛擬機(jī)將會把這些代碼編譯成與本地平臺相關(guān)的機(jī)器碼,并進(jìn)行各種層次的優(yōu)化,完成這個(gè)任務(wù)的編譯器稱為即時(shí)編譯器(JIT)。
即時(shí)編譯器不是虛擬機(jī)必須的部分,Java 虛擬機(jī)規(guī)范并沒有規(guī)定虛擬機(jī)內(nèi)部必須要有即時(shí)編譯器存在,更沒有限定或指導(dǎo)即時(shí)編譯器應(yīng)該如何實(shí)現(xiàn)。但是 JIT 編譯性能的好壞、代碼優(yōu)化程度的高低卻是衡量一款商用虛擬機(jī)優(yōu)秀與否的最關(guān)鍵指標(biāo)之一。
解釋器與編譯器
盡管并不是所有的 Java 虛擬機(jī)都采用解釋器與編譯器并存的架構(gòu),但許多主流的商用虛擬機(jī),如 HotSpot、J9 等,都同時(shí)包含解釋器與編譯器。
編譯器:負(fù)責(zé)把一種編程語言編寫的源碼轉(zhuǎn)換成另外一種計(jì)算機(jī)代碼,后者往往是以二進(jìn)制的形式被稱為目標(biāo)代碼(object code)。這個(gè)轉(zhuǎn)換的過程通常的目的是生成可執(zhí)行的程序。編譯器,往往是在「執(zhí)行」之前完成,產(chǎn)出是一種可執(zhí)行或需要再編譯或者解釋的「代碼」。
解釋器:它直接執(zhí)行由編程語言或腳本語言編寫的代碼,并不會把源代碼預(yù)編譯成機(jī)器碼。它是把程序源代碼一行一行的讀懂然后執(zhí)行,發(fā)生在運(yùn)行時(shí),產(chǎn)物是「運(yùn)行結(jié)果」。
解釋器與編譯器兩者各有優(yōu)勢:
當(dāng)程序需要迅速啟動(dòng)和執(zhí)行的時(shí)候,解釋器可以首先發(fā)揮作用,省去編譯的時(shí)間,立即執(zhí)行。在程序運(yùn)行后,隨著時(shí)間的推移,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地機(jī)器碼之后,可以獲得更高的執(zhí)行效率。
當(dāng)程序運(yùn)行環(huán)境中內(nèi)存資源限制較大(如部分嵌入式系統(tǒng)),可以使用解釋器執(zhí)行來節(jié)約內(nèi)存,反之可以使用編譯執(zhí)行來提升效率。
同時(shí),解釋器還可以作為編譯器激進(jìn)優(yōu)化時(shí)的一個(gè)「逃生門」,當(dāng)編譯器根據(jù)概率選擇一些大多數(shù)時(shí)候都能提升運(yùn)行速度的優(yōu)化手段,當(dāng)激進(jìn)優(yōu)化的假設(shè)不成立,如加載了新的類后類型繼承結(jié)構(gòu)出現(xiàn)變化、出現(xiàn)「罕見陷阱」時(shí)可以通過逆優(yōu)化退回到解釋狀態(tài)繼續(xù)執(zhí)行。
編譯對象與觸發(fā)條件
程序在運(yùn)行過程中會被即時(shí)編譯器編譯的「熱點(diǎn)代碼」有兩類:
被多次調(diào)用的方法;
被多次執(zhí)行的循環(huán)體。
這兩種被多次重復(fù)執(zhí)行的代碼,稱之為「熱點(diǎn)代碼」。
對于被多次調(diào)用的方法,方法體內(nèi)的代碼自然會被執(zhí)行多次,理所當(dāng)然的就是熱點(diǎn)代碼。
而對于多次執(zhí)行的循環(huán)體則是為了解決一個(gè)方法只被調(diào)用一次或者少量幾次,但是方法體內(nèi)部存在循環(huán)次數(shù)較多的循環(huán)體問題,這樣循環(huán)體的代碼也被重復(fù)執(zhí)行多次,因此這些代碼也是熱點(diǎn)代碼。
對于第一種情況,由于是方法調(diào)用觸發(fā)的編譯,因此編譯器理所當(dāng)然地會以整個(gè)方法作為編譯對象,這種編譯也是虛擬機(jī)中標(biāo)準(zhǔn)的 JIT 編譯方式。而對于后一種情況,盡管編譯動(dòng)作是由循環(huán)體所觸發(fā)的,但是編譯器依然會以整個(gè)方法(而不是單獨(dú)的循環(huán)體)作為編譯對象。這種編譯方式因?yàn)榘l(fā)生在方法執(zhí)行過程中,因此形象地稱之為棧上替換(On Stack Replacement,簡稱 OSR 編譯,即方法棧幀還在棧上,方法就被替換了)。
我們反復(fù)提到多次,可是多少次算多次呢?虛擬機(jī)如何統(tǒng)計(jì)一個(gè)方法或一段代碼被執(zhí)行過多少次呢?回答了這兩個(gè)問題,也就回答了即時(shí)編譯器的觸發(fā)條件。
判斷一段代碼是不是熱點(diǎn)代碼,是不是需要觸發(fā)即時(shí)編譯,這樣的行為稱為「熱點(diǎn)探測」。其實(shí)進(jìn)行熱點(diǎn)探測并不一定需要知道方法具體被調(diào)用了多少次,目前主要的熱點(diǎn)探測判定方式有兩種。
基于采樣的熱點(diǎn)探測:采用這種方法的虛擬機(jī)會周期性地檢查各個(gè)線程棧頂,如果發(fā)現(xiàn)某個(gè)(或某些)方法經(jīng)常出現(xiàn)在棧頂,那這個(gè)方法就是「熱點(diǎn)方法」。基于采樣的熱點(diǎn)探測的好處是實(shí)現(xiàn)簡單、高效,還可以很容易地獲取方法調(diào)用關(guān)系(將調(diào)用棧展開即可),缺點(diǎn)是很難精確地確認(rèn)一個(gè)方法的熱度,容易因?yàn)槭艿骄€程阻塞或別的外界因數(shù)的影響而擾亂熱點(diǎn)探測。
基于計(jì)數(shù)器的熱點(diǎn)探測:采用這種方法的虛擬機(jī)會為每個(gè)方法(甚至代碼塊)建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過一定的閾值就認(rèn)為它是「熱點(diǎn)方法」。這種統(tǒng)計(jì)方法實(shí)現(xiàn)起來麻煩一些,需要為每個(gè)方法建立并維護(hù)計(jì)數(shù)器,而且不能直接獲取到方法的調(diào)用關(guān)系,但是統(tǒng)計(jì)結(jié)果相對來說更加精確和嚴(yán)謹(jǐn)。
HotSpot 虛擬機(jī)采用的是第二種:基于計(jì)數(shù)器的熱點(diǎn)探測。因此它為每個(gè)方法準(zhǔn)備了兩類計(jì)數(shù)器:方法調(diào)用計(jì)數(shù)器(Invocation Counter)和回邊計(jì)數(shù)器(Back Edge Counter)。
在確定虛擬機(jī)運(yùn)行參數(shù)的情況下,這兩個(gè)計(jì)數(shù)器都有一個(gè)確定的閾值,當(dāng)計(jì)數(shù)器超過閾值就會觸發(fā) JIT 編譯。
方法調(diào)用計(jì)數(shù)器
顧名思義,這個(gè)計(jì)數(shù)器用于統(tǒng)計(jì)方法被調(diào)用的次數(shù)。當(dāng)一個(gè)方法被調(diào)用時(shí),會首先檢查該方法是否存在被 JIT 編譯過的版本,如果存在,則優(yōu)先使用編譯后的本地代碼來執(zhí)行。如果不存在,則將此方法的調(diào)用計(jì)數(shù)器加 1,然后判斷方法調(diào)用計(jì)數(shù)器與回邊計(jì)數(shù)器之和是否超過方法調(diào)用計(jì)數(shù)器的閾值。如果超過閾值,將會向即時(shí)編譯器提交一個(gè)該方法的代碼編譯請求。
如果不做任何設(shè)置,執(zhí)行引擎不會同步等待編譯請求完成,而是繼續(xù)進(jìn)入解釋器按照解釋方式執(zhí)行字節(jié)碼,直到提交的請求被編譯器編譯完成。當(dāng)編譯完成后,這個(gè)方法的調(diào)用入口地址就會被系統(tǒng)自動(dòng)改寫成新的,下一次調(diào)用該方法時(shí)就會使用已編譯的版本。
如果不做任何設(shè)置,方法調(diào)用計(jì)數(shù)器統(tǒng)計(jì)的并不是方法被調(diào)用的絕對次數(shù),而是一個(gè)相對的執(zhí)行頻率,即一段時(shí)間內(nèi)方法調(diào)用的次數(shù)。當(dāng)超過一定的時(shí)間限度,如果方法的調(diào)用次數(shù)仍然不足以讓它提交給即時(shí)編譯器編譯,那這個(gè)方法的調(diào)用計(jì)數(shù)器值就會被減少一半,這個(gè)過程稱為方法調(diào)用計(jì)數(shù)器熱度的衰減,而這段時(shí)間就稱為此方法統(tǒng)計(jì)的半衰期。
進(jìn)行熱度衰減的動(dòng)作是在虛擬機(jī)進(jìn)行 GC 時(shí)順便進(jìn)行的,可以設(shè)置虛擬機(jī)參數(shù)來關(guān)閉熱度衰減,讓方法計(jì)數(shù)器統(tǒng)計(jì)方法調(diào)用的絕對次數(shù),這樣,只要系統(tǒng)運(yùn)行時(shí)間足夠長,絕大部分方法都會被編譯成本地代碼。此外還可以設(shè)置虛擬機(jī)參數(shù)調(diào)整半衰期的時(shí)間。
回邊計(jì)數(shù)器
回邊計(jì)數(shù)器的作用是統(tǒng)計(jì)一個(gè)方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為「回邊」(Back Edge)。建立回邊計(jì)數(shù)器統(tǒng)計(jì)的目的是為了觸發(fā) OSR 編譯。
當(dāng)解釋器遇到一條回邊指令時(shí),會先查找將要執(zhí)行的代碼片段是否已經(jīng)有編譯好的版本,如果有,它將優(yōu)先執(zhí)行已編譯的代碼,否則就把回邊計(jì)數(shù)器值加 1,然后判斷方法調(diào)用計(jì)數(shù)器和回邊計(jì)數(shù)器值之和是否超過計(jì)數(shù)器的閾值。當(dāng)超過閾值時(shí),將會提交一個(gè) OSR 編譯請求,并且把回邊計(jì)數(shù)器的值降低一些,以便繼續(xù)在解釋器中執(zhí)行循環(huán),等待編譯器輸出編譯結(jié)果。
與方法計(jì)數(shù)器不同,回邊計(jì)數(shù)器沒有計(jì)算熱度衰減的過程,因此這個(gè)計(jì)數(shù)器統(tǒng)計(jì)的就是該方法循環(huán)執(zhí)行的絕對次數(shù)。當(dāng)計(jì)數(shù)器溢出時(shí),它還會把方法計(jì)數(shù)器的值也調(diào)整到溢出狀態(tài),這樣下次再進(jìn)入該方法的時(shí)候就會執(zhí)行標(biāo)準(zhǔn)編譯過程。
2.2 編譯優(yōu)化技術(shù)
我們都知道,以編譯方式執(zhí)行本地代碼比解釋執(zhí)行方式更快,一方面是因?yàn)楣?jié)約了虛擬機(jī)解釋執(zhí)行字節(jié)碼額外消耗的時(shí)間;另一方面是因?yàn)樘摂M機(jī)設(shè)計(jì)團(tuán)隊(duì)幾乎把所有對代碼的優(yōu)化措施都集中到了即時(shí)編譯器中。這一小節(jié)我們來介紹下 HotSpot 虛擬機(jī)的即時(shí)編譯器在編譯代碼時(shí)采用的優(yōu)化技術(shù)。
優(yōu)化技術(shù)概覽
代碼優(yōu)化技術(shù)有很多,實(shí)現(xiàn)這些優(yōu)化也很有難度,但是大部分還是比較好理解的。為了便于介紹,我們先從一段簡單的代碼開始,看看虛擬機(jī)會做哪些代碼優(yōu)化。
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
y = b.get();
z = b.get();
sum = y + z;
}
首先需要明確的是,這些代碼優(yōu)化是建立在代碼的某種中間表示或者機(jī)器碼上的,絕不是建立在 Java 源碼上。這里之所使用 Java 代碼來介紹是為了方便演示。
上面這段代碼看起來簡單,但是有許多可以優(yōu)化的地方。
第一步是進(jìn)行方法內(nèi)聯(lián)(Method Inlining),方法內(nèi)聯(lián)的重要性要高于其它優(yōu)化措施。方法內(nèi)聯(lián)的目的主要有兩個(gè),一是去除方法調(diào)用的成本(比如建立棧幀),二是為其它優(yōu)化建立良好的基礎(chǔ),方法內(nèi)聯(lián)膨脹之后可以便于更大范圍上采取后續(xù)的優(yōu)化手段,從而獲得更好的優(yōu)化效果。因此,各種編譯器一般都會把內(nèi)聯(lián)優(yōu)化放在優(yōu)化序列的最前面。內(nèi)聯(lián)優(yōu)化后的代碼如下:
public void foo() {
y = b.value;
z = b.value;
sum = y + z;
}
第二步進(jìn)行冗余消除,代碼中「z = b.value;」可以被替換成「z = y」。這樣就不用再去訪問對象 b 的局部變量。如果把 b.value 看做是一個(gè)表達(dá)式,那也可以把這項(xiàng)優(yōu)化工作看成是公共子表達(dá)式消除。優(yōu)化后的代碼如下:
public void foo() {
y = b.value;
z = y;
sum = y + z;
}
第三步進(jìn)行復(fù)寫傳播,因?yàn)檫@段代碼里沒有必要使用一個(gè)額外的變量 z,它與變量 y 是完全等價(jià)的,因此可以使用 y 來代替 z。復(fù)寫傳播后的代碼如下:
public void foo() {
y = b.value;
y = y;
sum = y + y;
}
第四步進(jìn)行無用代碼消除。無用代碼可能是永遠(yuǎn)不會執(zhí)行的代碼,也可能是完全沒有意義的代碼。因此,又被形象的成為「Dead Code」。上述代碼中 y = y 是沒有意義的,因此進(jìn)行無用代碼消除后的代碼是這樣的:
public void foo() {
y = b.value;
sum = y + y;
}
經(jīng)過這四次優(yōu)化后,最新優(yōu)化后的代碼和優(yōu)化前的代碼所達(dá)到的效果是一致的,但是優(yōu)化后的代碼執(zhí)行效率會更高。編譯器的這些優(yōu)化技術(shù)實(shí)現(xiàn)起來是很復(fù)雜的,但是想要理解它們還是很容易的。接下來我們再講講如下幾項(xiàng)最有代表性的優(yōu)化技術(shù)是如何運(yùn)作的,它們分別是:
公共子表達(dá)式消除;
數(shù)組邊界檢查消除;
方法內(nèi)聯(lián);
逃逸分析。
公共子表達(dá)式消除
如果一個(gè)表達(dá)式 E 已經(jīng)計(jì)算過了,并且從先前的計(jì)算到現(xiàn)在 E 中所有變量的值都沒有發(fā)生變化,那么 E 的這次出現(xiàn)就成了公共子表達(dá)式。對于這種表達(dá)式,沒有必要花時(shí)間再對它進(jìn)行計(jì)算,只需要直接使用前面計(jì)算過的表達(dá)式結(jié)果代替 E 就好了。如果這種優(yōu)化僅限于程序的基本塊內(nèi),便稱為局部公共子表達(dá)式消除,如果這種優(yōu)化的范圍覆蓋了多個(gè)基本塊,那就稱為全局公共子表達(dá)式消除。
數(shù)組邊界檢查消除
如果有一個(gè)數(shù)組 array[],在 Java 中訪問數(shù)組元素 array[i] 的時(shí)候,系統(tǒng)會自動(dòng)進(jìn)行上下界的范圍檢查,即檢查 i 必須滿足 i >= 0 && i < array.length,否則會拋出一個(gè)運(yùn)行時(shí)異常:java.lang.ArrayIndexOutOfBoundsException,這就是數(shù)組邊界檢查。
對于虛擬機(jī)執(zhí)行子系統(tǒng)來說,每次數(shù)組元素的讀寫都帶有一次隱含的條件判定操作,對于擁有大量數(shù)組訪問的程序代碼,這是一種不小的性能開銷。為了安全,數(shù)組邊界檢查是必須做的,但是數(shù)組邊界檢查并不一定每次都要進(jìn)行。比如在循環(huán)的時(shí)候訪問數(shù)組,如果編譯器只要通過數(shù)據(jù)流分析就知道循環(huán)變量是不是在區(qū)間 [0, array.length] 之內(nèi),那在整個(gè)循環(huán)中就可以把數(shù)組的上下界檢查消除。
方法內(nèi)聯(lián)
方法內(nèi)聯(lián)前面已經(jīng)通過代碼分析介紹過,這里就不再贅述了。
逃逸分析
逃逸分析不是直接優(yōu)化代碼的手段,而是為其它優(yōu)化手段提供依據(jù)的分析技術(shù)。逃逸分析的基本行為就是分析對象的動(dòng)態(tài)作用域:當(dāng)一個(gè)對象在方法中被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其它方法中,稱為方法逃逸。甚至還有可能被外部線程訪問到,例如賦值給類變量或可以在其他線程中訪問的實(shí)例變量,稱為線程逃逸。
如果能證明一個(gè)對象不會逃逸到方法或者線程之外,也就是別的方法和線程無法通過任何途徑訪問到這個(gè)方法,則可能為這個(gè)變量進(jìn)行一些高效優(yōu)化。比如:
棧上分配:如果確定一個(gè)對象不會逃逸到方法之外,那么就可以在棧上分配內(nèi)存,對象所占的內(nèi)存空間就可以隨棧幀出棧而銷毀。通常,不會逃逸的局部對象所占的比例很大,如果能棧上分配就會大大減輕 GC 的壓力。
同步消除:如果逃逸分析能確定一個(gè)變量不會逃逸出線程,無法被其它線程訪問,那這個(gè)變量的讀寫就不會有多線程競爭的問題,因而變量的同步措施也就可以消除了。
標(biāo)量替換:標(biāo)量是指一個(gè)數(shù)據(jù)無法再拆分成更小的數(shù)據(jù)來表示了,Java 虛擬機(jī)中的原始數(shù)據(jù)類型都不能再進(jìn)一步拆分,所以它們就是標(biāo)量。相反,一個(gè)數(shù)據(jù)可以繼續(xù)分解,那它就稱作聚合量,Java 中的對象就是聚合量。如果把一個(gè) Java 對象拆散,根據(jù)訪問情況將其使用到的成員變量恢復(fù)成原始類型來訪問,就叫標(biāo)量替換。如果逃逸分析證明一個(gè)對象不會被外部訪問,并且這個(gè)對象可以被拆散,那程序執(zhí)行的時(shí)候就可能不創(chuàng)建這個(gè)對象,而改為直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用到的成員變量來替代。對象被拆分后,除了可以讓對象的成員變量在棧上分配和讀寫,還可以為后續(xù)進(jìn)一步的優(yōu)化手段創(chuàng)造條件。
總結(jié)
以上是生活随笔為你收集整理的java程序编译_Java程序的编译过程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 黄酒的“酒龄十年”和“基酒十年”区别是什
- 下一篇: java日志服务器_java服务器搭建(