1 什么是編譯
“編譯”這個(gè)詞匯在各種關(guān)于編程語言的資料中都能看到,那究竟什么是編譯呢?簡(jiǎn)單地說,編譯是一個(gè)行為,是一個(gè)將一種語言翻譯成另一種語言的行為,而實(shí)現(xiàn)這個(gè)行為的東西就是“編譯器”。例如,C語言編譯器會(huì)將C語言源代碼翻譯成匯編代碼,然后再由匯編器將匯編代碼翻譯成機(jī)器可以直接識(shí)別的機(jī)器代碼。
如果按照是否有編譯這個(gè)過程將編程語言分類的話,大致可以分為兩大類:編譯型語言和解釋型語言。編譯型語言的典型代表就是C和C++,解釋型語言的典型代表是Python和JavaScript。那么Java屬于哪一種類型呢?說實(shí)話,我不確定,因?yàn)镴ava先經(jīng)過javac編譯后形成字節(jié)碼,然后JVM再解釋執(zhí)行字節(jié)碼,從這個(gè)角度看,好像Java可以歸到解釋型語言,但由于Java中也存在JIT即時(shí)編譯機(jī)制,將字節(jié)碼編譯成機(jī)器碼,然后機(jī)器再執(zhí)行機(jī)器碼,從這個(gè)角度看,好像又可以認(rèn)為Java是編譯型的。但我個(gè)人更傾向于“Java”是編譯型語言,因?yàn)閖avac將Java源代碼編譯成了字節(jié)碼,字節(jié)碼和源代碼已經(jīng)有非常大的不同了,換句話說,編譯的程度很深(而且javac編譯器也會(huì)包括編譯器該有的功能,例如詞法分析、語法分析、語義分析等),故我認(rèn)為Java是編譯型語言。
在網(wǎng)上看到有一條啟發(fā)性原則用來判斷語言的類型:如果翻譯器對(duì)程序進(jìn)行了徹底的分析而非某種機(jī)械的變換,而且生成的中間程序與源程序之間沒有很強(qiáng)的相似性,我們就認(rèn)為這個(gè)語言是編譯的。徹底的分析和非平凡的變換,是編譯方式的標(biāo)志性特征。
2 Java編譯器
Java大概有三類編譯器:前端編譯器、后端編譯器、靜態(tài)提前編譯器。
- 前端編譯器。典型代表是javac,作用是將Java源代碼編譯成虛擬機(jī)可以識(shí)別的字節(jié)碼,通俗的說就是將.java文件轉(zhuǎn)換成.class文件(這個(gè)說法有些狹隘了,實(shí)際上,不一定就是.class文件,只要是虛擬機(jī)可執(zhí)行的字節(jié)碼即可)。
- 后端編譯器。即我們經(jīng)??吹降摹癑IT”編譯器,典型代表是HotSpot的Client編譯器和Server編譯器,簡(jiǎn)稱為C1和C2,作用就是將字節(jié)碼轉(zhuǎn)換成機(jī)器可識(shí)別的機(jī)器碼。
- 靜態(tài)提前編譯器。典型代表是GNU Compiler for the Java,作用是在前端編譯器編譯之前,直接將java源代碼編譯成機(jī)器可識(shí)別的機(jī)器碼。
2.1 前端編譯器
我們通常所說的“編譯”大多少數(shù)時(shí)候都是指的“前端編譯”(僅限于Java領(lǐng)域)。前端編譯的主要工作是將java源碼編譯成字節(jié)碼,供虛擬機(jī)解釋執(zhí)行,但虛擬機(jī)規(guī)范并沒有對(duì)具體的編譯過程做嚴(yán)格要求,這就導(dǎo)致了各個(gè)編譯器可能大相徑庭,對(duì)于Javac來說,大致可以分為3個(gè)過程,分別是:
- 解析與填充符號(hào)表的過程。即解析Java源代碼,并將其中內(nèi)容轉(zhuǎn)換成符合表示并插入符號(hào)表里,詳細(xì)的過程會(huì)在下面說到。
- 插入式注解處理器的注解處理過程。Java5之后提供了注解,注解有可能會(huì)導(dǎo)致原代碼的部分邏輯發(fā)生改變,所以在這個(gè)過程會(huì)對(duì)注解做處理,并回到第一個(gè)過程,再次做解析與符號(hào)表填充。
- 分析與字節(jié)碼的生成。即生成最終可被虛擬機(jī)識(shí)別并解釋執(zhí)行的字節(jié)碼。
2.1.1 解析與符號(hào)表填充
解析包括詞法分析和語法分析,幾乎所有的編譯器都至少有這么兩步,通過這兩個(gè)步驟,會(huì)生成抽象語法樹,后續(xù)的過程都是基于抽象語法樹的,不會(huì)再直接對(duì)源代碼進(jìn)行操作。
詞法分析及即將源代碼的字符流轉(zhuǎn)換成一個(gè)一個(gè)的Token,Token被定義成一個(gè)不可再拆分的元素。例如對(duì)于int a = 5;來說,int、a、=、5、;等都是Token,且Token并不一定是一個(gè)字符。詞法分析是基于Java語言的規(guī)范來進(jìn)行的,這也就是為什么三個(gè)字符的int被認(rèn)為是一個(gè)Token的原因。
語法分析會(huì)根據(jù)詞法分析得到的Token序列來構(gòu)造抽象語法樹,抽象語法樹是一種樹形的數(shù)據(jù)結(jié)構(gòu),樹中的每個(gè)節(jié)點(diǎn)都代表程序代碼中的一個(gè)語法結(jié)構(gòu),例如包、類、修飾符、運(yùn)算符等。
完成詞法和語法分析后就算是完成了解析過程,接下來就是符號(hào)表的填充了。符號(hào)表是一組符號(hào)和符號(hào)地址組成的表,可以理解成Hash表,但實(shí)際上不一定就是Hash表的格式,也有可能是樹形或者其他形式,只要能表示符號(hào)和符號(hào)地址的映射關(guān)系即可,符號(hào)表的信息在后面的各個(gè)階段都有可能用到,例如在語義分析中,這些信息會(huì)被用來做語法檢查和生成中間代碼。
2.1.2 注解處理器
在編譯期對(duì)注解進(jìn)行處理的過程中,可能會(huì)修改抽象語法樹的節(jié)點(diǎn),所以在完成對(duì)注解的處理之后,還需要回到解析和符號(hào)表填充的過程,再次生成新的抽象語法樹,這樣一個(gè)循環(huán)可能會(huì)發(fā)生多次,直到注解處理器不會(huì)在修改抽象語法樹。
2.1.3 語義分析和字節(jié)碼生成
完成了上面的步驟之后,抽象語法樹的信息就不會(huì)再發(fā)生改變了,即此時(shí)的抽象語法樹是一個(gè)最終版本的抽象語法樹,但抽象語法樹只能表示程序是正確的,是符合語法要求的,但并不表示程序是符合邏輯的,所以還需要對(duì)其進(jìn)行語義分析來確定程序是否符合邏輯,分析的項(xiàng)目大概有標(biāo)注檢查、數(shù)據(jù)流和控制流分析等。這些檢查完成后會(huì)著手“解語法糖”,最后才會(huì)生成字節(jié)碼。
上面提到了“語法糖”這個(gè)東西,語法糖是用來方便程序員的,提高程序員的開發(fā)效率的。但本質(zhì)上對(duì)程序性能上沒有什么增益。例如For-each循環(huán)在編譯后展開成以迭代器的方式遍歷,基本類型的自動(dòng)裝箱和拆箱操作也會(huì)在編譯后展開成valueOf(),或者xxxValue()的方法調(diào)用。
2.1.4 前端編譯器的優(yōu)化操作
前端編譯器也會(huì)做一些優(yōu)化操作,但比起后端編譯器來說,優(yōu)化的力度比較小。在這里簡(jiǎn)單說一下兩個(gè)優(yōu)化操作:常量折疊和條件編譯。
常量折疊是一個(gè)將常量簡(jiǎn)化的優(yōu)化操作。例如現(xiàn)在有如下代碼:
int a = 2 + 3;
編譯器如果有常量折疊這項(xiàng)優(yōu)化的話,會(huì)將這條語句優(yōu)化成下面這樣:
int a = 5;
這樣就可以讓JVM少執(zhí)行一次加法指令,提高執(zhí)行效率。
條件編譯即將一些條件判斷的步驟省略,讓JVM少執(zhí)行一次條件判斷指令。例如:
public static void main(String[] args) {
if (true) {
System.out.println("block1");
} else {
System.out.println("block2")
}
}
如果編譯器又這么一項(xiàng)優(yōu)化的話,可能就會(huì)將代碼編譯成這樣:
public static void main(String[] args) {
System.out.println("block1");
}
這樣就少了一個(gè)條件判斷的指令,執(zhí)行效率就提高了。這種優(yōu)化只會(huì)發(fā)生在條件變量是常量的時(shí)候才行,如果條件變量可能發(fā)生改變,那么編譯器就不會(huì)做這項(xiàng)優(yōu)化。
2.2 后端編譯器
在Java中,我們通常所說的后端編譯就是說的JIT(即時(shí)編譯)。在HotSpot虛擬機(jī)中,有兩個(gè)即時(shí)編譯器,即Client Compiler和Server Compiler,簡(jiǎn)稱為C1和C2,這兩種編譯器對(duì)應(yīng)著虛擬機(jī)的運(yùn)行模式,如果虛擬機(jī)的運(yùn)行模式是Client,那么就使用C1,如果是Server模式,就使用C2,虛擬機(jī)的運(yùn)行模式可以通過如下命令看到。
> java -v
2.2.1 編譯器和解釋器
自從有了JIT,Java代碼就不再全是又虛擬機(jī)解釋執(zhí)行的了,而是一部分代碼繼續(xù)使用虛擬機(jī)解釋器解釋執(zhí)行,一部分代碼被編譯為機(jī)器碼,機(jī)器直接執(zhí)行機(jī)器碼。這樣可以發(fā)揮解釋器和編譯器的優(yōu)勢(shì),當(dāng)程序需要快速啟動(dòng)的時(shí)候,解釋器可以省去編譯過程(但之前講到的前端編譯生成字節(jié)碼的步驟仍然是必須的),直接解釋執(zhí)行程序,當(dāng)程序運(yùn)行后,編譯器可以將一些“熱點(diǎn)代碼”編譯成機(jī)器碼,以提高運(yùn)行時(shí)的執(zhí)行效率。
解釋器還能作為編譯器的“逃生門”,當(dāng)編譯失敗或者編譯后的代碼出現(xiàn)運(yùn)行問題(這通常是因?yàn)榫幾g器“激進(jìn)”的優(yōu)化操作導(dǎo)致的)時(shí)可以進(jìn)行“逆優(yōu)化”操作,此時(shí)虛擬機(jī)將繼續(xù)以解釋的模式運(yùn)行這部分代碼,這使得即使編譯失敗也不會(huì)突然導(dǎo)致運(yùn)行中的應(yīng)用程序崩潰。下面是解釋器和編譯器的交互示意圖:
由于即時(shí)編譯需要在程序運(yùn)行時(shí)執(zhí)行,必然會(huì)占用程序的資源,要編譯出優(yōu)化程度高的代碼,需要的資源可能會(huì)很多。HotSpot虛擬機(jī)提供了分層編譯的策略來緩解這個(gè)問題,大致可分為3層:程序解釋執(zhí)行,C1編譯、C2編譯。3層的優(yōu)化程度以此遞增,需要占用的資源也以此遞增,但將原來所需要的更大的資源分為了三個(gè)部分,虛擬機(jī)完全可以在不同的時(shí)間段里執(zhí)行三個(gè)過程,最終生成優(yōu)化程度很高的代碼。
2.2.2 JIT的觸發(fā)條件
上面的討論中提到過一個(gè)“熱點(diǎn)代碼”的概念,虛擬機(jī)不會(huì)將所有的字節(jié)碼都編譯成機(jī)器碼(因?yàn)橛行┐a可能僅僅會(huì)執(zhí)行那么一次兩次,編譯這部分代碼有點(diǎn)得不償失),而僅僅將部分經(jīng)常被使用的代碼編譯成機(jī)器碼,這部分代碼就稱作“熱點(diǎn)代碼”。
判斷一段代碼是不是熱點(diǎn)代碼,是不是需要進(jìn)行即使編譯,這樣的過程稱作“熱點(diǎn)探測(cè)”。目前主流的熱點(diǎn)探測(cè)方法有兩種:
- 基于采樣的熱點(diǎn)探測(cè)。使用這種方式的虛擬機(jī)會(huì)周期性的檢查棧頂,如果發(fā)現(xiàn)某個(gè)方法經(jīng)常出現(xiàn)在棧頂,那這個(gè)方法就被判斷為熱點(diǎn)代碼。這種方式的好處是實(shí)現(xiàn)簡(jiǎn)單、高效,還可以通過展開棧來獲得調(diào)用關(guān)系,缺點(diǎn)就是結(jié)果可能不準(zhǔn)確,容易受到例如線程阻塞或者外部因素的干擾。
- 基于計(jì)數(shù)器的熱點(diǎn)探測(cè)。使用這種方式的虛擬機(jī)會(huì)為每個(gè)方法創(chuàng)建計(jì)數(shù)器,當(dāng)方法被調(diào)用的時(shí)候,對(duì)應(yīng)的計(jì)數(shù)器就加1,當(dāng)計(jì)數(shù)器的值達(dá)到某個(gè)閾值的時(shí)候,就會(huì)判斷該方法為熱點(diǎn)方代碼,可以對(duì)其觸發(fā)即使編譯。這樣的好處是結(jié)果準(zhǔn)確,但無法獲得方法的調(diào)用關(guān)系。
HotSpot虛擬機(jī)采用的是第二種方法,它為每個(gè)方法準(zhǔn)備了兩個(gè)計(jì)數(shù)器:方法調(diào)用計(jì)數(shù)器和回邊計(jì)數(shù)器。
- 方法調(diào)用計(jì)數(shù)器。每當(dāng)方法被調(diào)用的時(shí)候,就加+1。
- 回邊計(jì)數(shù)器。作用是統(tǒng)計(jì)一個(gè)方法體里的循環(huán)體的執(zhí)行次數(shù)。
這兩個(gè)計(jì)數(shù)器的閾值并不一樣,只要有一個(gè)計(jì)數(shù)器達(dá)到閾值,就會(huì)觸發(fā)即時(shí)編譯,而且都會(huì)編譯整個(gè)方法,即使僅僅因?yàn)槔锩娴难h(huán)體是熱點(diǎn)代碼。
即時(shí)編譯和解釋執(zhí)行時(shí)可以并發(fā)執(zhí)行的,即編譯還沒完成的時(shí)候,解釋器仍然以解釋執(zhí)行的方式執(zhí)行代碼,當(dāng)編譯完成后再選擇運(yùn)行編譯后的代碼。下圖是方法調(diào)用計(jì)數(shù)器觸發(fā)即時(shí)編譯的流程圖(回邊計(jì)數(shù)器觸發(fā)的流程也相差不多):
關(guān)于JIT具體編譯的過程和細(xì)節(jié)就不多說了,書上寫得很詳細(xì)(但也比較晦澀),建議看看書上的第11章。
3 編譯優(yōu)化技術(shù)
HotSpot虛擬機(jī)在即時(shí)編譯方面有很多優(yōu)化技術(shù),其中也有不少經(jīng)典的優(yōu)化技術(shù),例如常量折疊、條件編譯等,也有一些針對(duì)Java的優(yōu)化技術(shù),例如棧上替換,逃逸分析等。下面介紹幾種比較具有代表性的優(yōu)化技術(shù)。
3.1 公共子表達(dá)式消除
這是一種普遍的優(yōu)化技術(shù),他的描述是這樣的:如果一個(gè)表達(dá)式E已經(jīng)計(jì)算過了,并且從先前到現(xiàn)在E都沒有發(fā)生過改變,那么E的這次出現(xiàn)就成為了公共子表達(dá)式,對(duì)于這種表達(dá)式就沒必要花費(fèi)時(shí)間再次計(jì)算了,只需要用先前計(jì)算的結(jié)果替代即可。假設(shè)有如下代碼:
int d = (c * b) * 12 + a + (a + b * c);
其中cxb和bxc是等效的,故將其看做E,編譯器就可以做出類似下面的優(yōu)化。
int d = E * 12 + a + (a + E);
甚至如果編譯器還有“代數(shù)化簡(jiǎn)”的優(yōu)化項(xiàng)目的話,可能會(huì)變成下面這樣:
int d = E * 13 + a * 2;
這樣一來,原來至少有6個(gè)算數(shù)運(yùn)算以及若干個(gè)括號(hào)相關(guān)、若干個(gè)算法運(yùn)算法則相關(guān)的壓棧和出棧操作變成了只有3個(gè)算數(shù)運(yùn)算操作,最終效率就變高。
3.2 數(shù)組邊界檢查消除
Java在對(duì)數(shù)組訪問的時(shí)候會(huì)對(duì)數(shù)組邊界進(jìn)行檢查,如果發(fā)生數(shù)組越界了,會(huì)拋出java.lang.ArrayIndexOutOfBoudnsException異常,而不會(huì)像C/C++那樣要么出現(xiàn)Segment Falut,要么就會(huì)出現(xiàn)亂碼,這一點(diǎn)對(duì)程序員來說是一件很好的事情,即使程序員沒有專門編寫防御性代碼,也可以避免很多內(nèi)存溢出攻擊,但正是由于多了邊界檢查,所以程序的運(yùn)行效率肯定也會(huì)受到影響。
為了降低數(shù)組邊界檢查的影響,編譯器可能會(huì)進(jìn)行“數(shù)組邊界檢查消除”的優(yōu)化,注意,這個(gè)優(yōu)化并不是完全將數(shù)組邊界檢查這個(gè)特性拋棄,而是對(duì)沒有必要進(jìn)行數(shù)組邊界檢查的數(shù)組訪問操作進(jìn)行檢查消除。例如對(duì)于數(shù)組A進(jìn)行A[3]的訪問操作,如果能在編譯期根據(jù)數(shù)據(jù)流分析得到A.length的值,并判斷出3小于A.length,那么在執(zhí)行訪問操作的時(shí)候就不需要對(duì)數(shù)組邊界進(jìn)行檢查了。再舉個(gè)例子,例如我們現(xiàn)在有如下代碼遍歷數(shù)組A:
for (int i = 0; i < A.length; i++) {
System.out.println(A[i]);
}
這段代碼中,循環(huán)遍歷i的值完全可以在編譯期確定就在[0,A.lenght)這個(gè)范圍里,而這個(gè)范圍沒有發(fā)生數(shù)組越界,故編譯器可以對(duì)這段代碼做數(shù)組邊界檢查消除的優(yōu)化,從而提高執(zhí)行效率。
除了數(shù)組邊界優(yōu)化之外,還有一種技術(shù)也可以降低隱式開銷:隱式異常處理。我們?cè)诰帉慗ava代碼的時(shí)候,經(jīng)常需要處理異常,Java程序在運(yùn)行時(shí)對(duì)異常的處理要做更多的事情(各種的檢查、判斷),所以,為了降低這種開銷,編譯器可能會(huì)對(duì)異常做一些“特殊處理”。假設(shè)有下面這樣的代碼:
public static void main(String[] args) {
User user = getUser("yenono");
System.out.println(user.getName());
}
虛擬機(jī)在執(zhí)行的時(shí)候會(huì)對(duì)user對(duì)象做空值判斷,Java偽代碼如下所示:
if (user != null) {
System.out.println(user.getName());
} else {
throw new NullPointException();
}
如果編譯器有“隱式異常處理”這項(xiàng)優(yōu)化的話,可能就會(huì)變成下面這樣(Java偽代碼):
try {
System.out.println(user.getName());
} catch (segment_fault) {
uncommon_trap();
}
比起原來的代碼,少了判斷過程,直接就對(duì)對(duì)象進(jìn)行操作了。當(dāng)確實(shí)發(fā)生異常的時(shí)候,就不得不從用戶態(tài)陷入到內(nèi)核態(tài)去處理該異常,處理完成之后再回到用戶態(tài)繼續(xù)執(zhí)行,這個(gè)過程的效率遠(yuǎn)遠(yuǎn)比一次空值判斷低得多。所以,當(dāng)很少發(fā)生異常的情況下,應(yīng)用程序能從這項(xiàng)優(yōu)化中獲益,但如果經(jīng)常發(fā)生異常,這樣的優(yōu)化反而會(huì)使得應(yīng)用程序的效率更低,不過好在虛擬機(jī)足夠智能,可以通過運(yùn)行時(shí)的各種信息來自動(dòng)的選擇最好的方案。
這里提到了異常會(huì)陷入內(nèi)核態(tài),這是因?yàn)樘摂M機(jī)會(huì)在操作系統(tǒng)中注冊(cè)一個(gè)segment_fault異常,注冊(cè)完畢后,該異常就屬于操作系統(tǒng)異常了,當(dāng)虛擬機(jī)捕獲到這個(gè)異常的時(shí)候,就會(huì)發(fā)生中斷陷入到內(nèi)核態(tài)中對(duì)異常進(jìn)行處理,處理完畢后又從內(nèi)核態(tài)切換回用戶態(tài)繼續(xù)執(zhí)行后面的邏輯。
3.3 方法內(nèi)聯(lián)
學(xué)過C++的朋友應(yīng)該都接觸過“內(nèi)聯(lián)函數(shù)”,即那些有inline關(guān)鍵字標(biāo)識(shí)的函數(shù)。在C++中,函數(shù)內(nèi)聯(lián)可以簡(jiǎn)單理解成將整個(gè)函數(shù)當(dāng)做一個(gè)代碼塊,當(dāng)其他函數(shù)調(diào)用的時(shí)候,就直接把這個(gè)代碼塊復(fù)制到調(diào)用該函數(shù)的地方,最終的效果就是沒有發(fā)生函數(shù)調(diào)用,也就是少了一次函數(shù)的入棧和出棧操作,這樣的做法對(duì)性能的增益確實(shí)挺大的,尤其是對(duì)C++這種靜態(tài)編譯的語言,方法內(nèi)聯(lián)的過程完全可以在編譯期就完成了,在運(yùn)行時(shí)就不會(huì)再做復(fù)制代碼的操作了。
在Java中,無法進(jìn)行如此直接的方法內(nèi)聯(lián),因?yàn)镴ava的多態(tài)實(shí)在運(yùn)行時(shí)實(shí)現(xiàn)的,不像C++那樣在編譯期就確定了虛函數(shù)表以及C++對(duì)象的虛函數(shù)指針。所以java在編譯期進(jìn)行方法內(nèi)聯(lián)幾乎無法完成,因?yàn)楦緹o法確定最終調(diào)用的方法是哪一個(gè)版本,不過對(duì)于有fianl修飾的方法(無法被重寫),倒是可以進(jìn)行方法內(nèi)聯(lián),但總不可能為了性能,到處使用final修飾方法吧。那Java究竟是如何實(shí)現(xiàn)方法內(nèi)聯(lián)優(yōu)化的呢?虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)引入了一種“類型繼承關(guān)系分析(Class Hierarchy Analysis,CHA)”的技術(shù),說實(shí)話,這個(gè)技術(shù)我完全沒弄明白,所以在這里就不多說了,要想細(xì)致了解的,建議看看《深入理解Java虛擬機(jī)》中11.3.4節(jié)的內(nèi)容。
3.4 逃逸分析
逃逸分析不是直接的優(yōu)化技術(shù),而是為其他優(yōu)化提供依據(jù)的分析技術(shù)。那什么是逃逸呢?當(dāng)在一個(gè)方法里創(chuàng)建了一個(gè)對(duì)象,然后再在該方法里調(diào)用其他方法并將對(duì)象作為參數(shù)傳遞到被調(diào)用方法里,這個(gè)就是對(duì)象“逃逸”了,這種方式的逃逸稱作方法逃逸。在多線并發(fā)的環(huán)境中,還有線程逃逸的說法,那指的是某個(gè)對(duì)象被其他線程訪問到,詳細(xì)的可以看看《Java并發(fā)編程》中線程安全那一章節(jié)。
如果用逃逸分析技術(shù)分析某個(gè)對(duì)象不會(huì)發(fā)生逃逸,那么就可以不將對(duì)象分配到堆里,而是在棧上分配對(duì)象,反正這個(gè)對(duì)象又不會(huì)被其他方法調(diào)用到,僅在本方法里使用。在棧上分配的好處是線程安全、不需要垃圾回收和可以進(jìn)行標(biāo)量替換:
- 線程安全。因?yàn)闂J蔷€程私有的,對(duì)象分配在棧上了,自然就不可能被其他線程共享了,也就不會(huì)有線程安全問題了。
- 不需要垃圾回收。當(dāng)方法棧幀出棧以后,這部分棧內(nèi)存就自動(dòng)釋放了,自然不需要進(jìn)行垃圾回收了,這就減輕了垃圾回收的壓力。
- 標(biāo)量替換。標(biāo)量是指一個(gè)數(shù)據(jù)已經(jīng)無法再分解成更小的數(shù)據(jù)類型來表示了,例如基本數(shù)據(jù)類型int,long以及引用類型等,相應(yīng)的,一個(gè)數(shù)據(jù)如果能繼續(xù)分解成更小的數(shù)據(jù)類型,那就稱作聚合量,例如Java對(duì)象。根據(jù)對(duì)象的訪問狀況將其成員變量恢復(fù)原始類型的訪問就叫做標(biāo)量替換。例如某方法里用到一個(gè)user對(duì)象,而且方法里僅僅方法了user對(duì)象的name,和age字段,那么編譯器就可以對(duì)其做一個(gè)標(biāo)量替換,直接為name和age創(chuàng)建兩個(gè)局部變量,而不需要再創(chuàng)建user對(duì)象了,減少了創(chuàng)建對(duì)象的開銷。
逃逸分析是一個(gè)比較前沿的技術(shù),在JDK1.6中才有實(shí)現(xiàn),到現(xiàn)如今也并不是很成熟。原因就是因?yàn)闊o法保證逃逸分析帶來的性能提升大于逃逸分析本身的消耗,畢竟逃逸分析是一個(gè)很復(fù)雜的過程,也是非常耗時(shí)的。一種比較極端的情況就是,經(jīng)過一頓復(fù)雜的分析,最后發(fā)現(xiàn)該方法里的所有對(duì)象都會(huì)逃逸,這樣基于逃逸分析的優(yōu)化操作就無法做了,白白浪費(fèi)時(shí)間來做這這些分析。
4 小結(jié)
本文簡(jiǎn)單介紹了前端編譯器和后端編譯器,最后還說了幾個(gè)具有代表性的編譯優(yōu)化技術(shù)。編譯器和編譯優(yōu)化技術(shù)看起來距離我們很遠(yuǎn)(對(duì)于普通的應(yīng)用開發(fā)者),但理解它們是絕對(duì)有益處的,例如我知道了編譯器會(huì)幫我將公共子表達(dá)式消除了,我在編寫代碼的時(shí)候就不需要老關(guān)注是否存在公共子表達(dá)式了(因?yàn)槿绻壿嫳容^復(fù)雜的話,這個(gè)過程是非常煩人的),提升了開發(fā)效率并且對(duì)程序執(zhí)行效率沒有影響。記得知乎上曾經(jīng)有過一個(gè)問題:C/C++的i++和++i的寫法性能上有什么差別?看過《CSAPP》的朋友應(yīng)該知道++i的寫法效率上會(huì)比較好(具體原因在這里就不多說了),但實(shí)際上呢?編譯器會(huì)給我們優(yōu)化!所以編譯后這兩種寫法沒有區(qū)別!如果了解編譯器的優(yōu)化技術(shù)的話,在實(shí)際開發(fā)中,就不用糾結(jié)這樣的問題了,根據(jù)公司的代碼風(fēng)格使用其中一種就行了(最好不要混搭,否則太混亂了)。
5 參考資料
《深入理解Java虛擬機(jī)》

