JVM(十三):后端編譯優(yōu)化

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í)行邏輯分別為如下所示:

方法調(diào)用計(jì)數(shù)器

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

回邊調(diào)用計(jì)數(shù)器

經(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í)行邏輯如下所示:

方法內(nèi)聯(lián)過程

公共子表達(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公眾號

文章在公眾號「iceWang」第一手更新,有興趣的朋友可以關(guān)注公眾號,第一時(shí)間看到筆者分享的各項(xiàng)知識點(diǎn),謝謝!筆芯!

本系列文章主要借鑒自《深入分析 JavaWeb 技術(shù)內(nèi)幕》和《深入理解 Java 虛擬機(jī)-JVM 高級特性與最佳實(shí)踐》。

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

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

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