JVM(十三):后端編譯優(yōu)化
在 JVM(一):源文件的轉(zhuǎn)變 中我們介紹了 Java 中的前端優(yōu)化,即將 Java 源代碼轉(zhuǎn)換為字節(jié)碼文件。在本文中,我們將介紹字節(jié)碼文件如何轉(zhuǎn)換為本地機(jī)器碼,并如何對代碼進(jìn)行優(yōu)化,以提高性能。因?yàn)椴煌奶摂M機(jī),字節(jié)碼優(yōu)化引擎不同,因此本文采用 JIT 來作為例子,其也是 HotSpot 中的默認(rèn)編譯器。
架構(gòu)
我們都知道將代碼轉(zhuǎn)換為機(jī)器碼有兩種方式,而在 HotSpot 中采用了卻兩者全部都涉及到了,其采用了解釋器和編譯器并存的架構(gòu)。那么其這樣的目的是什么呢?
首先我們知道解釋執(zhí)行,可以大大提高程序啟動(dòng)時(shí)的效率,因?yàn)樵谶@個(gè)時(shí)候需要執(zhí)行什么代碼,才對對應(yīng)的源碼進(jìn)行翻譯,將其變?yōu)闄C(jī)器碼,因此也提高了啟動(dòng)時(shí)效率;
而編譯執(zhí)行的優(yōu)點(diǎn)則是可以獲得更高的執(zhí)行效率,因?yàn)槠鋵⒅虚g代碼全部編譯成了與機(jī)器相關(guān)的本地代碼,并且在這一階段,有些編譯器還會對編譯后的代碼進(jìn)行初步的優(yōu)化,這也使得效率更加的優(yōu)秀。
因此 Hotspot 開始執(zhí)行的時(shí)候采用解釋執(zhí)行,獲得優(yōu)良的啟動(dòng)效率,而在代碼執(zhí)行過程中,對執(zhí)行情況進(jìn)行監(jiān)控,運(yùn)用以前所說的 熱點(diǎn)代碼編譯技術(shù) 將熱點(diǎn)代碼編譯成本地機(jī)器碼,并根據(jù)執(zhí)行情況進(jìn)行優(yōu)化,以獲得兩者全部的優(yōu)點(diǎn)。
可能會有讀者問道,我的代碼部署在服務(wù)器上,第一次慢一點(diǎn)就慢一點(diǎn),我只采用編譯執(zhí)行不行嗎?
其實(shí)不然,首先因?yàn)榫幾g器需要對代碼進(jìn)行優(yōu)化,因此肯定是執(zhí)行過程中根據(jù)執(zhí)行情況進(jìn)行優(yōu)化更加的好,此外,在優(yōu)化的過程中也有一種 激進(jìn)優(yōu)化 的方式,在這種情況下,就需要采用解釋執(zhí)行的方式通過 逆優(yōu)化 的方式來退回到解釋狀態(tài)來執(zhí)行了。
因此,兩種方式并存的架構(gòu)是合理且必要的,也因此目前主流的虛擬機(jī)也大多采取這種架構(gòu)。
即時(shí)編譯器
HotSpot 中的即時(shí)編譯器有兩種,分別稱為 Client Complier 和 Server Complier,或者簡稱為 C1 和 C2,目前虛擬機(jī)一般采用解釋器和一個(gè)即時(shí)編譯器直接配合的方式來運(yùn)行,這種模式稱之為 混合模式。
既然是兩者合作,那么久需要考慮一個(gè)調(diào)度的問題,即何時(shí)使用編譯執(zhí)行,何時(shí)采用解釋執(zhí)行,多少的比例可以獲得最佳平衡,得到最高的效率。
在 HotSpot 中是通過 分層編譯 的策略來達(dá)到最優(yōu)解的。其本質(zhì)的思想如下所示:
- 第0層:程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控,觸發(fā)第一層;
- 第1層:C1 編譯,將字節(jié)碼編譯為本地代碼,進(jìn)行簡單、可靠的優(yōu)化,如果有必要,可以加入性能監(jiān)控;
- 第2層:C2 編譯,也是將字節(jié)碼編譯為本地代碼,但其會啟動(dòng)一些耗時(shí)較長的優(yōu)化,甚至?xí)鶕?jù)監(jiān)控的信息采取一些激進(jìn)的優(yōu)化措施。
這種分層編譯的方式可以達(dá)到一定情況的最優(yōu)解:用 C1 獲取更快的編譯速度,用 C2 獲取更好的編譯質(zhì)量,解釋執(zhí)行的時(shí)候也無需增加性能監(jiān)控的任務(wù),反而拖累了啟動(dòng)效率。
編譯對象
因?yàn)榫幾g的過程是一個(gè)耗時(shí)耗力的工作,因此對那么頻繁執(zhí)行的代碼進(jìn)行編譯能獲得更高的提升。因此如何判斷那么代碼是 熱點(diǎn)代碼 呢?
在 JVM 中,熱點(diǎn)代碼的判斷方式有兩種:
-
基于采樣,周期性的檢查棧頂,如果一段代碼頻繁出現(xiàn)在棧幀頂部,那么就判斷其是熱點(diǎn)代碼。
- 優(yōu)點(diǎn):實(shí)現(xiàn)簡單,快;
- 缺點(diǎn):探測很容易收到線程阻塞的影響。例如一個(gè)方法因?yàn)榫€程阻塞,一直在棧頂,但其實(shí)其執(zhí)行次數(shù)并不多,那么將其判定為熱點(diǎn)代碼就是不合理的。
-
基于計(jì)數(shù)器:為每個(gè)方法甚至是代碼塊建立計(jì)數(shù)器來統(tǒng)計(jì)執(zhí)行次數(shù),如果統(tǒng)計(jì)的次數(shù)達(dá)到了一定的條件則說明是熱點(diǎn)代碼
- 優(yōu)點(diǎn):結(jié)果精確
- 缺點(diǎn):實(shí)現(xiàn)就比較麻煩了,需要維護(hù)計(jì)數(shù)器
HotSpot 中采取的是第二種方案,因?yàn)轭l繁執(zhí)行的代碼有如下兩種:
- 方法的頻繁執(zhí)行
- 一段代碼的頻繁執(zhí)行
因此 HotSpot 中建立了兩種類型的計(jì)數(shù)器來進(jìn)行判斷,其執(zhí)行邏輯分別為如下所示:

回邊的判斷方法與方法基本一致,只是在提交編譯請求后,需要把回邊計(jì)數(shù)器的值減小一點(diǎn),保證代碼以解釋狀態(tài)繼續(xù)執(zhí)行。

經(jīng)典優(yōu)化方案
JIT 中有太多編譯的優(yōu)化技術(shù)了,在這里我們就找?guī)讉€(gè)比較經(jīng)典的介紹一個(gè),剩下的讀者感興趣可以 Google 一下,或者給作者留言,可以再拓展一篇文章單獨(dú)介紹一下。
方法內(nèi)聯(lián)
方法內(nèi)聯(lián)應(yīng)該是 Java 中最重要的幾項(xiàng)優(yōu)化技術(shù)之一,其存在的最大意義就是為其他優(yōu)化手段提供了基礎(chǔ)。其使得代碼膨脹,因此也提供了更多的優(yōu)化機(jī)會。
表面來看,方法內(nèi)聯(lián)只是將代碼復(fù)制一份到調(diào)用的地方,但實(shí)現(xiàn)起來真的那么簡單嗎?
前面我們說過方法的多態(tài)調(diào)用,介紹了只有 非虛方法 可以在編譯期間知道調(diào)用的是哪個(gè)版本的方法,但是像虛方法這種,是可能會存在多個(gè)版本可以選擇的,那么編譯器在進(jìn)行方法內(nèi)聯(lián)的時(shí)候,該復(fù)制哪里的代碼呢?
為了解決這個(gè)問題,JVM 團(tuán)隊(duì)引入了 類型繼承關(guān)系分析 的技術(shù)。這種技術(shù)的執(zhí)行邏輯如下所示:

公共子表達(dá)式消除
如果一個(gè)表達(dá)式,在兩次計(jì)算過程中,其內(nèi)所有變量的值并沒有發(fā)生變化,那么則將其稱為公共子表達(dá)式。
舉個(gè)栗子:
int a = (b*c)*4+(c*b+d)+d
上面這段代碼在計(jì)算 b*c 的兩次中并沒有變化,因此可以將其簡寫為int a = E * 4 + (E + d) + d,再進(jìn)一步還可以進(jìn)行 代數(shù)化簡 優(yōu)化,將其優(yōu)化為:
int a = E * 5 + 2 * d;
數(shù)組范圍檢查消除
Java 語言中為了保持代碼的健壯和安全性,在每次數(shù)組訪問的時(shí)候需要判斷其是否在 0 ~ length-1 的范圍內(nèi),如若不然,將拋出異常。這樣做有個(gè)顯而易見的好處是可以提高程序的健壯性,但這對于擁有大量數(shù)組訪問的程序來說,就是一個(gè)災(zāi)難了。
因此,如果可以確保數(shù)組訪問不會越界的情況下,JVM 則可以做出相應(yīng)的優(yōu)化,例如可以使用隱式異常處理。栗子如下:
if(object != null){
return object.value;
}else{
throw new Exception();
}
在確定如果 object 在大多數(shù)情況下不會為空后,可以做出以下優(yōu)化:
try{
return object.value;
}catch(segment_fault){
exception_execute;
....
}
這樣就可以減少大量的判斷開銷。
逃逸分析
逃逸分析可以說是目前最前沿的優(yōu)化技術(shù)。其是指當(dāng)分析對象作用域時(shí),如果一個(gè)對象在被定義后,其不會外部方法和線程訪問到,那么就可以說明其是不會逃逸的,即其生命周期只有在被定義的塊中,因此就可以對其進(jìn)行優(yōu)化。
- 棧上分配,Java 對象大家都知道是分配在堆上的,但通過前面的學(xué)習(xí),我們知道棧上的對象在管理時(shí),是十分地影響性能的。因此我們考慮,既然其不會逃逸的話,那么直接將其分配到棧上不是更好嗎。這樣其可以隨著線程的消亡而消亡,減少垃圾收集的壓力;
- 同步消除,如果對象不會逃逸,就別談線程不安全的訪問了,也就不會被多個(gè)線程訪問,因此沒有必要對其進(jìn)行同步,直接可以把同步消除掉;
- 標(biāo)量替換,Java 中的對象分為 標(biāo)量 和 聚合量 ,其中標(biāo)量是不能再被拆分的變量,如 int、long 等。而聚合量中最典型的就是對象,現(xiàn)在如果能判斷對象不會逃逸,因此結(jié)合棧上分配,將其拆分為標(biāo)量然后分配到棧上是一個(gè)很好的優(yōu)化方式。
前面說了那么多逃逸分析的優(yōu)點(diǎn),但目前逃逸分析技術(shù)還并不是十分的成熟,其能夠帶來的優(yōu)化效果還不好說。
例如下面這種極端情況,JVM 在經(jīng)過逃逸分析后,發(fā)現(xiàn)所有的對象都是可以逃逸出去的,那么就帶來的性能消耗就十分的不值了,因?yàn)楫吘固右莘治鍪且粋€(gè)相對高耗時(shí)的過程,耗費(fèi)了大量的時(shí)間和運(yùn)算資源,結(jié)果發(fā)現(xiàn)全部白費(fèi)了。
不過雖然如此,但筆者相信逃逸分析一定是一個(gè)優(yōu)化的技術(shù)發(fā)展路線。因?yàn)槠浣?jīng)過優(yōu)化后的代碼大大提升了性能。
總結(jié)
在本文中,我們對后端編譯的方方面面進(jìn)行了分析,包括其編譯器架構(gòu),分層編譯的思想,如何判斷一段代碼值得被編譯為本地代碼,以及采取哪些方式來優(yōu)化代碼。
對這些內(nèi)容的深入理解,有助于我們在工作中分出哪些代碼是可以被編譯器優(yōu)化的,哪些是需要自己處理的,提高自身編碼效率。

文章在公眾號「iceWang」第一手更新,有興趣的朋友可以關(guān)注公眾號,第一時(shí)間看到筆者分享的各項(xiàng)知識點(diǎn),謝謝!筆芯!
本系列文章主要借鑒自《深入分析 JavaWeb 技術(shù)內(nèi)幕》和《深入理解 Java 虛擬機(jī)-JVM 高級特性與最佳實(shí)踐》。