第11章-晚期(運(yùn)行期)優(yōu)化

[TOC]

11.1 概述

  1. 在部分的商用虛擬機(jī)(Sun HotSpot槽奕、IBM J9)中嘴纺,Java 程序最初是通過解釋器(Interpreter)進(jìn)行解釋執(zhí)行的败晴,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊的運(yùn)行特別頻繁時(shí)栽渴,就會把這些代碼認(rèn)定為 “熱點(diǎn)代碼”(Hot Spot Code)尖坤。
  2. 為了提高熱點(diǎn)代碼的執(zhí)行效率墅冷,在運(yùn)行時(shí),虛擬機(jī)將會把這些代碼編譯成與本地平臺相關(guān)的機(jī)器碼叫编,并進(jìn)行各種層次的優(yōu)化辖佣,完成這個(gè)任務(wù)的編譯器稱為即時(shí)編譯器(Just In Time Compiler,下文中簡稱 JIT 編譯器)搓逾。
  3. 即時(shí)編譯器并不是虛擬機(jī)必需的部分卷谈,Java 虛擬機(jī)規(guī)范并沒有規(guī)定Java虛擬機(jī)內(nèi)必須要有即時(shí)編譯器存在,更沒有限定或指導(dǎo)即時(shí)編譯器應(yīng)該如何去實(shí)現(xiàn)恃逻。但是,即時(shí)編譯器編譯性能的好壞藕施、代碼優(yōu)化程度的高低卻是衡量一款商用虛擬機(jī)優(yōu)秀與否的最關(guān)鍵的指標(biāo)之一寇损,它也是虛擬機(jī)中最核心且最能體現(xiàn)虛擬機(jī)技術(shù)水平的部分。

11.2 HotSpot虛擬機(jī)內(nèi)的即時(shí)編譯器

  • 在本節(jié)中裳食,我們將要了解 HotSpot 虛擬機(jī)內(nèi)的即時(shí)編譯器的運(yùn)作過程矛市,同時(shí),還要解決以下幾個(gè)問題:
    • 為何 HotSpot 虛擬機(jī)要使用解釋器與編譯器并存的架構(gòu)诲祸?
    • 為何 HotSpot 虛擬機(jī)要實(shí)現(xiàn)兩個(gè)不同的即時(shí)編譯器浊吏?
    • 程序何時(shí)使用解釋器執(zhí)行?何時(shí)使用編譯器執(zhí)行救氯?
    • 哪些程序代碼會被編譯為本地代碼找田?如何編譯為本地代碼?

11.2.1 解釋器與編譯器

  1. 盡管并不是所有的 Java 虛擬機(jī)都采用解釋器與編譯器并存的架構(gòu)着憨,但許多主流的商用虛擬機(jī)墩衙,如HotSpot、J9等甲抖,都同時(shí)包含解釋器與編譯器漆改。
  2. 解釋器與編譯器兩者各有優(yōu)勢:
    • 當(dāng)程序需要迅速啟動和執(zhí)行的時(shí)候,解釋器可以首先發(fā)揮作用准谚,省去編譯的時(shí)間挫剑,立即執(zhí)行。
    • 在程序運(yùn)行后柱衔,隨著時(shí)間的推移樊破,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后唆铐,可以獲取更高的執(zhí)行效率捶码。
    • 當(dāng)程序運(yùn)行環(huán)境中內(nèi)存資源限制較大(如部分嵌入式系統(tǒng)中),可以使用解釋執(zhí)行節(jié)約內(nèi)存或链,反之可以使用編譯執(zhí)行來提升效率惫恼。
  3. 同時(shí),解釋器還可以作為編譯器激進(jìn)優(yōu)化時(shí)的一個(gè) “逃生門”澳盐,讓編譯器根據(jù)概率選擇一些大多數(shù)時(shí)候都能提升運(yùn)行速度的優(yōu)化手段祈纯,當(dāng)激進(jìn)優(yōu)化的假設(shè)不成立令宿,如加載了新類后類型繼承結(jié)構(gòu)出現(xiàn)變化、出現(xiàn) “罕見陷阱”(Uncommon Trap)時(shí)可以通過逆優(yōu)化(Deoptimization)退回到解釋狀態(tài)繼續(xù)執(zhí)行腕窥。因此粒没,在整個(gè)虛擬機(jī)執(zhí)行架構(gòu)中,解釋器與編譯器經(jīng)常配合工作簇爆。
解釋器與編譯器
  1. HotSpot 虛擬機(jī)中內(nèi)置了兩個(gè)即時(shí)編譯器癞松,分別稱為 Client CompilerServer Compiler,或者簡稱為 C1 編譯器和 C2 編譯器(也叫Opto編譯器)入蛆。
  2. 目前主流的 HotSpot 虛擬機(jī)中响蓉,默認(rèn)采用解釋器與其中一個(gè)編譯器直接配合的方式工作,程序使用哪個(gè)編譯器哨毁,取決于虛擬機(jī)運(yùn)行的模式枫甲。
  3. 無論采用的編譯器是 Client Compiler 還是 Server Compiler,解釋器與編譯器搭配使用的方式在虛擬機(jī)中稱為 “混合模式”(Mixed Mode)扼褪,可以通過虛擬機(jī)的 “-version” 命令的輸出結(jié)果顯示想幻。
  4. 由于即時(shí)編譯器編譯本地代碼需要占用程序運(yùn)行時(shí)間,為了在程序啟動響應(yīng)速度與運(yùn)行效率之間達(dá)到最佳平衡话浇,HotSpot 虛擬機(jī)還會逐漸啟用分層編譯(Tiered Compilation)的策略脏毯。分層編譯根據(jù)編譯器編譯、優(yōu)化的規(guī)模與耗時(shí)幔崖,劃分出不同的編譯層次抄沮,其中包括:
    • 第0層,程序解釋執(zhí)行岖瑰,解釋器不開啟性能監(jiān)控功能(Profiling)叛买,可觸發(fā)第1層編譯。
    • 第1層蹋订,也稱為 C1 編譯率挣,將字節(jié)碼編譯為本地代碼,進(jìn)行簡單露戒、可靠的優(yōu)化椒功,如有必要將加入性能監(jiān)控的邏輯。
    • 第2層(或2層以上)智什,也稱為 C2 編譯动漾,也是將字節(jié)碼編譯為本地代碼,但是會啟用一些編譯耗時(shí)較長的優(yōu)化荠锭,甚至?xí)鶕?jù)性能監(jiān)控信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化旱眯。

實(shí)施分層編譯后,Client Compiler和Server Compiler將會同時(shí)工作,許多代碼都可能會被多次編譯删豺,用Client Compiler獲取更高的編譯速度共虑,用Server Compiler來獲取更好的編譯質(zhì)量,在解釋執(zhí)行的時(shí)候也無須再承擔(dān)收集性能監(jiān)控信息的任務(wù)呀页。

11.2.2 編譯對象與觸發(fā)條件

  1. 在運(yùn)行過程中會被即時(shí)編譯器編譯的 “熱點(diǎn)代碼” 有兩類妈拌,即:
    • 被多次調(diào)用的方法。
    • 被多次執(zhí)行的循環(huán)體蓬蝶。
  2. 前者很好理解尘分,一個(gè)方法被調(diào)用得多了,方法體內(nèi)代碼執(zhí)行的次數(shù)自然就多丸氛,它成為 “熱點(diǎn)代碼” 是理所當(dāng)然的培愁。而后者則是為了解決一個(gè)方法只被調(diào)用過一次或少量的幾次,但是方法體內(nèi)部存在循環(huán)次數(shù)較多的循環(huán)體的問題雪位,這樣循環(huán)體的代碼也被重復(fù)執(zhí)行多次竭钝,因此這些代碼也應(yīng)該認(rèn)為是 “熱點(diǎn)代碼”梨撞。
  3. 對于第一種情況雹洗,由于是由方法調(diào)用觸發(fā)的編譯,因此編譯器理所當(dāng)然地會以整個(gè)方法作為編譯對象卧波,這種編譯也是虛擬機(jī)中標(biāo)準(zhǔn)的 JIT 編譯方式时肿。而對于后一種情況,盡管編譯動作是由循環(huán)體所觸發(fā)的港粱,但編譯器依然會以整個(gè)方法(而不是單獨(dú)的循環(huán)體)作為編譯對象螃成。這種編譯方式因?yàn)榫幾g發(fā)生在方法執(zhí)行過程之中,因此形象地稱之為棧上替換(On Stack Replacement查坪,簡稱為 OSR 編譯寸宏,即方法棧幀還在棧上,方法就被替換了)偿曙。
  4. 判斷一段代碼是不是熱點(diǎn)代碼氮凝,是不是需要觸發(fā)即時(shí)編譯,這樣的行為稱為熱點(diǎn)探測(Hot Spot Detection)望忆,目前主要的熱點(diǎn)探測判定方式有兩種罩阵,分別如下:
方式 描述 優(yōu)點(diǎn) 缺點(diǎn)
基于采樣的熱點(diǎn)探測 周期性地檢查各個(gè)線程的棧頂,如果發(fā)現(xiàn)某個(gè)方法經(jīng)常出現(xiàn)在棧頂启摄,那這個(gè)方法就是 “熱點(diǎn)方法” 實(shí)現(xiàn)簡單稿壁、高效,還可以很容易地獲取方法調(diào)用關(guān)系 很難精確地確認(rèn)一個(gè)方法的熱度
基于計(jì)數(shù)器的熱點(diǎn)探測 為每個(gè)方法(甚至是代碼塊)建立計(jì)數(shù)器歉备,執(zhí)行次數(shù)超過一定的閥值就認(rèn)為它是 “熱點(diǎn)方法” 實(shí)現(xiàn)麻煩傅是,需要為每個(gè)方法建立并維護(hù)計(jì)數(shù)器,而且不能直接獲取到方法的調(diào)用關(guān)系 統(tǒng)計(jì)結(jié)果相對來說更加精確和嚴(yán)謹(jǐn)
  1. 在 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 編譯界拦。

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

  1. 我們首先來看看方法調(diào)用計(jì)數(shù)器。顧名思義梗劫,這個(gè)計(jì)數(shù)器就用于統(tǒng)計(jì)方法被調(diào)用的次數(shù)享甸,它的默認(rèn)閥值在Client模式下是1500次,在Server模式下是10000次梳侨。當(dāng)一個(gè)方法被調(diào)用時(shí)蛉威,會進(jìn)行如下步驟:

    • 先檢查該方法是否存在被 JIT編譯過的版本,如果存在走哺,則優(yōu)先使用編譯后的本地代碼來執(zhí)行蚯嫌。
    • 如果不存在已被編譯過的版本,則將此方法的調(diào)用計(jì)數(shù)器值加1
    • 然后判斷方法調(diào)用計(jì)數(shù)器與回邊計(jì)數(shù)器值之和是否超過方法調(diào)用計(jì)數(shù)器的閾值丙躏。
    • 如果已超過閾值择示,那么將會向即時(shí)編譯器提交一個(gè)該方法的代碼編譯請求。
  2. 如果不做任何設(shè)置晒旅,執(zhí)行引擎并不會同步等待編譯請求完成栅盲,而是繼續(xù)進(jìn)入解釋器按照解釋方式執(zhí)行字節(jié)碼,直到提交的請求被編譯器編譯完成废恋。當(dāng)編譯工作完成之后谈秫,這個(gè)方法的調(diào)用入口地址就會被系統(tǒng)自動改寫成新的,下一次調(diào)用該方法時(shí)就會使用已編譯的版本鱼鼓。

方法調(diào)用計(jì)數(shù)器觸發(fā)即時(shí)編譯
  1. 如果不做任何設(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ù)器熱度的衰減(Counter Decay),而這段時(shí)間就稱為此方法統(tǒng)計(jì)的半衰周期(Counter Half Life Time)曹阔。進(jìn)行熱度衰減的動作是在虛擬機(jī)進(jìn)行垃圾收集時(shí)順便進(jìn)行的半开。

11.2.2.2 回邊計(jì)數(shù)器

  1. 現(xiàn)在我們再來看看另外一個(gè)計(jì)數(shù)器——回邊計(jì)數(shù)器,它的作用是統(tǒng)計(jì)一個(gè)方法中循環(huán)體代碼執(zhí)行的次數(shù)赃份,在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為 “回邊”(BackEdge)寂拆。顯然奢米,建立回邊計(jì)數(shù)器統(tǒng)計(jì)的目的就是為了觸發(fā) OSR 編譯。

  2. 需要設(shè)置一個(gè)參數(shù) -XX:OnStackReplacePercentage 來間接調(diào)整回邊計(jì)數(shù)器的閥值纠永,其計(jì)算公式如下:

    • 虛擬機(jī)運(yùn)行在 Client 模式下鬓长,回邊計(jì)數(shù)器閥值計(jì)算公式為:
      • 方法調(diào)用計(jì)數(shù)器閾值(Compile Threshold)× OSR比率(OnStackReplacePercentage)/100
      • 其中OnStackReplacePercentage默認(rèn)值為933,如果都取默認(rèn)值尝江,那Client模式虛擬機(jī)的回邊計(jì)數(shù)器的閥值為13995涉波。
    • 虛擬機(jī)運(yùn)行在 Server 模式下,回邊計(jì)數(shù)器閥值的計(jì)算公式為:
      • 方法調(diào)用計(jì)數(shù)器閥值(Compile Threshold)×(OSR比率(OnStackReplacePercentage)- 解釋器監(jiān)控比率(InterpreterProfilePercentage))/100
      • 其中OnStackReplacePercentage默認(rèn)值為140炭序,InterpreterProfilePercentage默認(rèn)值為33啤覆,如果都取默認(rèn)值,那Server模式虛擬機(jī)回邊計(jì)數(shù)器的閥值為10700惭聂。
  3. 當(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ù)器觸發(fā)即時(shí)編譯
  1. 與方法計(jì)數(shù)器不同牵舵,回邊計(jì)數(shù)器沒有計(jì)數(shù)熱度衰減的過程,因此這個(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)編譯過程方援。

11.2.3 編譯過程

  1. 在默認(rèn)設(shè)置下没炒,無論是方法調(diào)用產(chǎn)生的即時(shí)編譯請求,還是 OSR 編譯請求犯戏,虛擬機(jī)在代碼編譯器還未完成之前送火,都仍然將按照解釋方式繼續(xù)執(zhí)行,而編譯動作則在后臺的編譯線程中進(jìn)行先匪。
  2. 對于 Client Compiler 來說种吸,它是一個(gè)簡單快速的三段式編譯器,主要的關(guān)注點(diǎn)在于局部性的優(yōu)化呀非,而放棄了許多耗時(shí)較長的全局優(yōu)化手段坚俗。
    • 在第一個(gè)階段镜盯,一個(gè)平臺獨(dú)立的前端將字節(jié)碼構(gòu)造成一種高級中間代碼表示(High-Level Infermediate Representaion,HIR)猖败。
    • HIR 使用靜態(tài)單分配(Static Single Assignment速缆,SSA)的形式來代表代碼值,這可以使得一些在 HIR 的構(gòu)造過程之中和之后進(jìn)行的優(yōu)化動作更容易實(shí)現(xiàn)恩闻。
    • 在此之前編譯器會在字節(jié)碼上完成一部分基礎(chǔ)優(yōu)化艺糜,如方法內(nèi)聯(lián)、常量傳播等優(yōu)化將會在字節(jié)碼被構(gòu)造成HIR之前完成
    • 在第二個(gè)階段幢尚,一個(gè)平臺相關(guān)的后端從 HIR 中產(chǎn)生低級中間代碼表示(Low-Level Intermediate Representation倦踢,LIR),而在此之前會在 HIR上完成另外一些優(yōu)化侠草,如空值檢查消除辱挥、范圍檢查消除等育八,以便讓 HHR 達(dá)到更高效的代碼表示形式拭抬。
    • 最后階段是在平臺相關(guān)的后端使用線性掃描算法项炼,在LIR上分配寄存器呐籽,并在LIR上做窺孔(Peephole)優(yōu)化秩铆,然后產(chǎn)生機(jī)器代碼烙荷。
Client Compiler
  1. 而 Server Compiler 則是專門面向服務(wù)端的典型應(yīng)用并為服務(wù)端的性能配置特別調(diào)整過的編譯器浴骂,也是一個(gè)充分優(yōu)化過的高級編譯器度宦,它會執(zhí)行所有經(jīng)典的優(yōu)化動作式撼,如無用代碼消除(Dead Code Elimination)童社、循環(huán)展開(Loop Unrolling)、循環(huán)表達(dá)式外提(Loop Expression Hoisting)著隆、消除公共子表達(dá)式(Common Subexpression Elimination)扰楼、常量傳播(Constant Propagation)、基本塊重排序(Basic Block Reordering)等美浦,還會實(shí)施一些與 Java 語言特性密切相關(guān)的優(yōu)化技術(shù)弦赖,如范圍檢查消除(Range Check Elimination)、空值檢查消除(Null Check Elimination)等浦辨。另外蹬竖,還可能根據(jù)解釋器或 Client Compiler 提供的性能監(jiān)控信息,進(jìn)行一些不穩(wěn)定的激進(jìn)優(yōu)化流酬,如守護(hù)內(nèi)聯(lián)(Guarded Inlining)币厕、分支頻率預(yù)測(Branch Frequency Prediction)等。

11.3 編譯優(yōu)化技術(shù)

11.3.1 優(yōu)化技術(shù)概覽

// 原始代碼
static class B
{
    int value; 
    final int get()
    {
        return value; 
    }
}

public void foo()
{
    y = b.get();
    //... do stuff...
    z = b.get(); 
    sum = y + z;
}
  1. 上圖的代碼已經(jīng)非常簡單了芽腾,但是仍有許多優(yōu)化的余地旦装。第一步進(jìn)行方法內(nèi)聯(lián)(Method Inlining),方法內(nèi)聯(lián)的重要性要高于其他優(yōu)化措施晦嵌,它的主要目的有兩個(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)后的代碼如下:
//內(nèi)聯(lián)后的代碼
public void foo()
{
    y = b.value;
    //... do stuff...
    z = b.value; 
    sum = y + z;
}
  1. 第二步進(jìn)行冗余訪問消除(Redundant Loads Elimination),假設(shè)代碼中間注釋掉的 do stuff 所代表的操作不會改變 b.value 的值棒妨,那就可以把 z = b.value 替換為 z = y踪古,因?yàn)樯弦痪?y = b.value 已經(jīng)保證了變量 y 與 b.value 是一致的,這樣就可以不再去訪問對象 b 的局部變量了券腔。如果把 b.value 看做是一個(gè)表達(dá)式伏穆,那也可以把這項(xiàng)優(yōu)化看成是公共子表達(dá)式消除(Common Subexpression Elimination),優(yōu)化后的代碼如下:
public void foo()
{
    y = b.value;
    //... do stuff...
    z = y;
    sum = y + z;
}
  1. 第三步我們進(jìn)行復(fù)寫傳播(Copy Propagation)纷纫,因?yàn)樵谶@段程序的邏輯中并沒有必要使用一個(gè)額外的變量 “z”枕扫,它與變量 “y” 是完全相等的,因此可以使用 “y” 來代替 “z”辱魁。復(fù)寫傳播之后程序如下:
public void foo()
{
    y = b.value;
    //... do stuff...
    y = y;
    sum = y + y;
}
  1. 第四步我們進(jìn)行無用代碼消除(Dead Code Elimination)烟瞧。無用代碼可能是永遠(yuǎn)不會被執(zhí)行的代碼,也可能是完全沒有意義的代碼染簇,因此参滴,它又形象地稱為 “Dead Code”,在上述代碼中锻弓,y = y 是沒有意義的砾赔,把它消除后的程序如下:
public void foo()
{
    y = b.value;
    //... do stuff...
    sum = y + y;
}

11.3.2 公共子表達(dá)式消除

  1. 公共子表達(dá)式消除是一個(gè)普遍應(yīng)用于各種編譯器的經(jīng)典優(yōu)化技術(shù),它的含義是:如果一個(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á)式消除(Local Common Subexpression Elimination),如果這種優(yōu)化的范圍涵蓋了多個(gè)基本塊甚亭,那就稱為全局公共子表達(dá)式消除(Global Common Subexpression Elimination)贷币。
  2. int d =(c × b)× 12 + a +(a - b × c)
    如果這段代碼交給 Javac 編譯器則不會進(jìn)行任何優(yōu)化,而當(dāng)這段代碼進(jìn)入到虛擬機(jī)即時(shí)編譯器后亏狰,它將進(jìn)行如下優(yōu)化:
    int d = E × 12 + a +(a + E)役纹;
    這時(shí),編譯器還可能(取決于哪種虛擬機(jī)的編譯器以及具體的上下文而定)進(jìn)行另外一種優(yōu)化:代數(shù)化簡(Algebraic Simplification)暇唾,把表達(dá)式變?yōu)椋?br> int d = E × 13 + a × 2促脉;

11.3.3 數(shù)組邊界檢查消除

  1. 數(shù)組邊界檢查消除(Array Bounds Checking Elimination)是即時(shí)編譯器中的一項(xiàng)語言相關(guān)的經(jīng)典優(yōu)化技術(shù)辰斋。我們知道 Java 語言是一門動態(tài)安全的語言,對數(shù)組的讀寫訪問也不像C瘸味、C++那樣在本質(zhì)上是裸指針操作宫仗。如果有一個(gè)數(shù)組 foo[],在 Java 語言中訪問數(shù)組元素 foo[i] 的時(shí)候系統(tǒng)將會自動進(jìn)行上下界的范圍檢查旁仿,即檢查 i 必須滿足 i >= 0 && i < foo.length 這個(gè)條件藕夫,否則將拋出一個(gè)運(yùn)行時(shí)異常。
  2. 由于每次數(shù)組元素的讀寫都帶有一次隱含的條件判定操作枯冈,對于擁有大量數(shù)組訪問的程序代碼毅贮,這無疑也是一種性能負(fù)擔(dān)。雖然數(shù)組邊界檢查肯定是必須做的尘奏,但是不是必須在一次不漏地檢查則是可以 “商量” 的事情滩褥。
  3. 例如下面這個(gè)簡單的情況:數(shù)組下標(biāo)是一個(gè)常量,如 foo[3]炫加,只要在編譯期根據(jù)數(shù)據(jù)流分析來確定 foo.length的值铸题,并判斷下標(biāo) “3” 沒有越界,執(zhí)行的時(shí)候就無須判斷了琢感。
  4. 更加常見的情況是數(shù)組訪問發(fā)生在循環(huán)之中丢间,并且使用循環(huán)變量來進(jìn)行數(shù)組訪問,如果編譯器通過數(shù)據(jù)流分析判定循環(huán)變量的取值范圍永遠(yuǎn)在區(qū)間 [0驹针,foo.length)之內(nèi)烘挫,那在整個(gè)循環(huán)中就可以把數(shù)組的上下界檢查消除,這可以節(jié)省很多次的條件判斷操作柬甥。
  5. 除此之外饮六,Java 中還有很多的安全檢查,這些事情就成為一種隱式開銷苛蒲。要消除這些隱式開銷卤橄,除了如數(shù)組邊界檢查優(yōu)化這種盡可能把運(yùn)行期檢查提到編譯期完成的思路之外,另外還有一種避免思路——隱式異常處理臂外,Java中空指針檢查和算術(shù)運(yùn)算中除數(shù)為零的檢查都采用了這種思路窟扑。
  6. 例如程序中訪問一個(gè)對象(假設(shè)對象叫 foo)的某個(gè)屬性(假設(shè)屬性叫 value),那以 Java 偽代碼來表示虛擬機(jī)訪問 foo.value 的過程如下:
if (foo != null)
{
    return foo.value;
}
else
{
    throw new NullPointException();
}
  • 在使用隱式異常優(yōu)化之后漏健,虛擬機(jī)會把上面?zhèn)未a所表示的訪問過程變?yōu)槿缦聜未a:
try
{
    return foo.value;
}
catch (segment fault)
{
    uncommon_trap();
}
  1. 虛擬機(jī)會注冊一個(gè) Segment Fault 信號的異常處理器(偽代碼中的 uncommon_trap() )嚎货,這樣當(dāng) foo 不為空的時(shí)候,對 value 的訪問是不會額外消耗一次對 foo 判空的開銷的蔫浆。
  2. 然而代價(jià)就是當(dāng) foo 真的為空時(shí)殖属,必須轉(zhuǎn)入到異常處理器中恢復(fù)并拋出 NullPointException 異常,這個(gè)過程必須從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài)中處理瓦盛,結(jié)束后再回到用戶態(tài)洗显,速度遠(yuǎn)比一次判空檢查慢外潜。
  3. 當(dāng) foo 極少為空的時(shí)候,隱式異常優(yōu)化是值得的挠唆,但假如 foo 經(jīng)常為空的話橡卤,這樣的優(yōu)化反而會讓程序更慢,還好HotSpot 虛擬機(jī)足夠 “聰明”损搬,它會根據(jù)運(yùn)行期收集到的 Profile 信息自動選擇最優(yōu)方案碧库。

11.3.4 方法內(nèi)聯(lián)

  1. 在前面的講解之中我們提到過方法內(nèi)聯(lián),它是編譯器最重要的優(yōu)化手段之一巧勤,除了消除方法調(diào)用的成本之外嵌灰,它更重要的意義是為其他優(yōu)化手段建立良好的基礎(chǔ)。

  2. 然而按照經(jīng)典編譯原理的優(yōu)化理論颅悉,大多數(shù)的 Java 方法本身都無法進(jìn)行內(nèi)聯(lián)沽瞭,這是因?yàn)椋?/p>

    • 只有使用 invokespecial 指令調(diào)用的私有方法、實(shí)例構(gòu)造器剩瓶、父類方法以及使用 invokestatic 指令進(jìn)行調(diào)用的靜態(tài)方法才是在編譯期進(jìn)行解析的
    • 除了上述 4 種方法之外驹溃,其他的 Java 方法調(diào)用都需要在運(yùn)行時(shí)進(jìn)行方法接收者的多態(tài)選擇,并且都有可能存在多于一個(gè)版本的方法接收者(最多再除去被 final 修飾的方法這種特殊情況延曙,盡管它使用invokevirtual 指令調(diào)用豌鹤,但也是非虛方法)。簡而言之枝缔,Java語言中默認(rèn)的實(shí)例方法是虛方法布疙。
    • 而對于一個(gè)虛方法,編譯期做內(nèi)聯(lián)的時(shí)候根本無法確定應(yīng)該使用哪個(gè)方法版本愿卸,就是不依賴上下文就無法確定變量的實(shí)際類型是什么灵临。假如有 Parent 和 Sub 兩個(gè)具有繼承關(guān)系的類,并且子類重寫了父類的 get()方法趴荸,那么儒溉,是要執(zhí)行父類的 get() 方法還是子類的 get() 方法,需要在運(yùn)行期才能確定发钝,編譯期無法得出結(jié)論顿涣。
  3. 為了解決虛方法的內(nèi)聯(lián)問題,首先是引入了一種名為 “類型繼承關(guān)系分析”(Class Hierarchy Analysis笼平,CHA)的技術(shù)园骆,這是一種基于整個(gè)應(yīng)用程序的類型分析技術(shù),它用于確定在目前已加載的類中寓调,某個(gè)接口是否有多于一種的實(shí)現(xiàn),某個(gè)類是否存在子類锄码、子類是否為抽象類等信息夺英。

  4. 編譯器在進(jìn)行內(nèi)聯(lián)時(shí):

    • 如果是非虛方法晌涕,那么直接進(jìn)行內(nèi)聯(lián)就可以了,這時(shí)候的內(nèi)聯(lián)是有穩(wěn)定前提保障的痛悯。
    • 如果遇到虛方法余黎,則會向 CHA 查詢此方法在當(dāng)前程序下是否有多個(gè)目標(biāo)版本可供選擇:
      • 如果查詢結(jié)果只有一個(gè)版本,那也可以進(jìn)行內(nèi)聯(lián)载萌,不過這種內(nèi)聯(lián)就屬于激進(jìn)優(yōu)化惧财,需要預(yù)留一個(gè)“逃生門”(Guard條件不成立時(shí)的Slow Path),稱為守護(hù)內(nèi)聯(lián)(Guarded Inlining)扭仁。
      • 如果向 CHA 查詢出來的結(jié)果是有多個(gè)版本的目標(biāo)方法可供選擇垮衷,則編譯器還將會進(jìn)行最后一次努力,使用內(nèi)聯(lián)緩存(Inline Cache)來完成方法內(nèi)聯(lián)乖坠。
  5. 守護(hù)內(nèi)聯(lián):如果程序的后續(xù)執(zhí)行過程中搀突,虛擬機(jī)一直沒有加載到會令這個(gè)方法的接收者的繼承關(guān)系發(fā)生變化的類,那這個(gè)內(nèi)聯(lián)優(yōu)化的代碼就可以一直使用下去熊泵。但如果加載了導(dǎo)致繼承關(guān)系發(fā)生變化的新類仰迁,那就需要拋棄已經(jīng)編譯的代碼,退回到解釋狀態(tài)執(zhí)行顽分,或者重新進(jìn)行編譯徐许。

  6. 內(nèi)斂緩存:這是一個(gè)建立在目標(biāo)方法正常入口之前的緩存,它的工作原理大致是:

    • 在未發(fā)生方法調(diào)用之前卒蘸,內(nèi)聯(lián)緩存狀態(tài)為空
    • 當(dāng)?shù)谝淮握{(diào)用發(fā)生后绊寻,緩存記錄下方法接收者的版本信息
    • 之后每次進(jìn)行方法調(diào)用時(shí)都比較接收者版本
      • 如果以后進(jìn)來的每次調(diào)用的方法接收者版本都是一樣的,那這個(gè)內(nèi)聯(lián)還可以一直用下去悬秉。
      • 如果發(fā)生了方法接收者不一致的情況澄步,就說明程序真正使用了虛方法的多態(tài)特性,這時(shí)才會取消內(nèi)聯(lián)和泌,查找虛方法表進(jìn)行方法分派村缸。

11.3.5 逃逸分析

  1. 逃逸分析(Escape Analysis)是目前 Java 虛擬機(jī)中比較前沿的優(yōu)化技術(shù),它與類型繼承關(guān)系分析一樣武氓,并不是直接優(yōu)化代碼的手段梯皿,而是為其他優(yōu)化手段提供依據(jù)的分析技術(shù)。
  2. 逃逸分析的基本行為就是分析對象動態(tài)作用域:當(dāng)一個(gè)對象在方法中被定義后县恕,它可能被外部方法所引用东羹,例如作為調(diào)用參數(shù)傳遞到其他方法中,稱為方法逃逸忠烛。甚至還有可能被外部線程訪問到属提,譬如賦值給類變量或可以在其他線程中訪問的實(shí)例變量,稱為線程逃逸
  3. 如果能證明一個(gè)對象不會逃逸到方法或線程之外冤议,也就是別的方法或線程無法通過任何途徑訪問到這個(gè)對象斟薇,則可能為這個(gè)變量進(jìn)行一些高效的優(yōu)化,具體如下:
  4. 棧上分配(Stack Allocation):如果確定一個(gè)對象不會逃逸出方法之外恕酸,那讓這個(gè)對象在棧上分配內(nèi)存將會是一個(gè)很不錯(cuò)的主意堪滨,對象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀。在一般應(yīng)用中蕊温,不會逃逸的局部對象所占的比例很大袱箱,如果能使用棧上分配,那大量的對象就會隨著方法的結(jié)束而自動銷毀了义矛,垃圾收集系統(tǒng)的壓力將會小很多发笔。
  5. 同步消除(Synchronization Elimination):線程同步本身是一個(gè)相對耗時(shí)的過程,如果逃逸分析能夠確定一個(gè)變量不會逃逸出線程症革,無法被其他線程訪問筐咧,那這個(gè)變量的讀寫肯定就不會有競爭,對這個(gè)變量實(shí)施的同步措施也就可以消除掉噪矛。
  6. 標(biāo)量替換(Scalar Replacement):
    • 標(biāo)量(Scalar)是指一個(gè)數(shù)據(jù)已經(jīng)無法再分解成更小的數(shù)據(jù)來表示了量蕊,Java 虛擬機(jī)中的原始數(shù)據(jù)類型(int、long等數(shù)值類型以及 reference 類型等)都不能再進(jìn)一步分解艇挨,它們就可以稱為標(biāo)量残炮。
    • 相對的,如果一個(gè)數(shù)據(jù)可以繼續(xù)分解缩滨,那它就稱作聚合量(Aggregate)势就,Java中的對象就是最典型的聚合量
    • 如果把一個(gè) Java 對象拆散脉漏,根據(jù)程序訪問的情況苞冯,將其使用到的成員變量恢復(fù)原始類型來訪問就叫做標(biāo)量替換。
    • 如果逃逸分析證明一個(gè)對象不會被外部訪問侧巨,并且這個(gè)對象可以被拆散的話舅锄,那程序真正執(zhí)行的時(shí)候?qū)?strong>可能不創(chuàng)建這個(gè)對象,而改為直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用到的成員變量來代替司忱。將對象拆分后皇忿,除了可以讓對象的成員變量在棧上(棧上存儲的數(shù)據(jù),有很大的概率會被虛擬機(jī)分配至物理機(jī)器的高速寄存器中存儲)分配和讀寫之外坦仍,還可以為后續(xù)進(jìn)一步的優(yōu)化手段創(chuàng)建條件鳍烁。

11.4 Java 與 C/C++ 的編譯器對比

11.4.1 Java 編譯器的劣勢

  • Java 虛擬機(jī)的即時(shí)編譯器與 C/C++ 的靜態(tài)優(yōu)化編譯器相比,可能會由于下列這些原因而導(dǎo)致輸出的本地代碼有一些劣勢:
  1. 因?yàn)榧磿r(shí)編譯器運(yùn)行占用的是用戶程序的運(yùn)行時(shí)間繁扎,具有很大的時(shí)間壓力幔荒,它能提供的優(yōu)化手段也嚴(yán)重受制于編譯成本。如果編譯速度不能達(dá)到要求,那用戶將在啟動程序或程序的某部分察覺到重大延遲铺峭,這點(diǎn)使得即時(shí)編譯器不敢隨便引入大規(guī)模的優(yōu)化技術(shù)墓怀,而編譯的時(shí)間成本在靜態(tài)優(yōu)化編譯器中并不是主要的關(guān)注點(diǎn)汽纠。
  2. Java 語言是動態(tài)的類型安全語言卫键,這就意味著需要由虛擬機(jī)來確保程序不會違反語言語義或訪問非結(jié)構(gòu)化內(nèi)存。從實(shí)現(xiàn)層面上看虱朵,這就意味著虛擬機(jī)必須頻繁地進(jìn)行動態(tài)檢查莉炉,如實(shí)例方法訪問時(shí)檢查空指針、數(shù)組元素訪問時(shí)檢查上下界范圍碴犬、類型轉(zhuǎn)換時(shí)檢查繼承關(guān)系等絮宁。對于這類程序代碼沒有明確寫出的檢查行為,盡管編譯器會努力進(jìn)行優(yōu)化服协,但是總體上仍然要消耗不少的運(yùn)行時(shí)間绍昂。
  3. Java 語言中雖然沒有 virtual 關(guān)鍵字,但是使用虛方法的頻率卻遠(yuǎn)遠(yuǎn)大于 C/C++ 語言偿荷,這意味著運(yùn)行時(shí)對方法接收者進(jìn)行多態(tài)選擇的頻率要遠(yuǎn)遠(yuǎn)大于C/C++語言窘游,也意味著即時(shí)編譯器在進(jìn)行一些優(yōu)化(如前面提到的方法內(nèi)聯(lián))時(shí)的難度要遠(yuǎn)大于C/C++的靜態(tài)優(yōu)化編譯器。
  4. Java 語言是可以動態(tài)擴(kuò)展的語言跳纳,運(yùn)行時(shí)加載新的類可能改變程序類型的繼承關(guān)系忍饰,這使得很多全局的優(yōu)化都難以進(jìn)行,因?yàn)榫幾g器無法看見程序的全貌寺庄,許多全局的優(yōu)化措施都只能以激進(jìn)優(yōu)化的方式來完成艾蓝,編譯器不得不時(shí)刻注意并隨著類型的變化而在運(yùn)行旺撤銷或重新進(jìn)行一些優(yōu)化。
  5. Java 語言中對象的內(nèi)存分配都是堆上進(jìn)行的斗塘,只有方法中的局部變量才能在棧上分配赢织。而 C/C++ 的對象則有多種內(nèi)存分配方式,既可能在堆上分配馍盟,又可能在棧上分配于置,如果可以在棧上分配線程私有的對象,將減輕內(nèi)存回收的壓力朽合。另外俱两,C/C++ 中主要由用戶程序代碼來回收分配的內(nèi)存,這就不存在無用對象篩選的過程曹步,因此效率上(僅指運(yùn)行效率宪彩,排除了開發(fā)效率)也比垃圾收集機(jī)制要高。

11.4.2 Java 編譯器的優(yōu)勢

  • Java 語言的這些性能上的劣勢都是為了換取開發(fā)效率上的優(yōu)勢而付出的代價(jià)讲婚,動態(tài)安全尿孔、動態(tài)擴(kuò)展、垃圾回收這些 “拖后腿” 的特性都為 Java 語言的開發(fā)效率做出了很大貢獻(xiàn)。何況活合,還有許多優(yōu)化是 Java 的即時(shí)編譯器能做而 C/C++ 的靜態(tài)優(yōu)化編譯器不能做或者不好做的雏婶。
  1. 例如,在 C/C++ 中白指,別名分析(Alias Analysis)的難度就要遠(yuǎn)高于 Java留晚。Java 的類型安全保證了在類似如下代碼中,只要 ClassA 和 ClassB 沒有繼承關(guān)系告嘲,那對象 objA 和 objB 就絕不可能是同一個(gè)對象错维,即不會是同一塊內(nèi)存兩個(gè)不同別名
void foo (ClassA objA, ClassB objB)
{
    objA.x = 123;
    objB.y = 456;

    //只要 objB.y 不是 objA.x 的別名,下面就可以保證輸出為 123
    print(objA.x);
}
  1. Java 編譯器另外一個(gè)紅利是由它的動態(tài)性所帶來的橄唬,由于 C/C++ 編譯器所有優(yōu)化都在編譯期完成赋焕,以運(yùn)行期性能監(jiān)控為基礎(chǔ)的優(yōu)化措施它都無法進(jìn)行,如調(diào)用頻率預(yù)測(Call Frequency Prediction)仰楚、分支頻率預(yù)測(Branch Frequency Prediction)隆判、裁剪未被選擇的分支(Untaken Branch Pruning)等,這些都會成為 Java 語言獨(dú)有的性能優(yōu)勢僧界。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末侨嘀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子捎泻,更是在濱河造成了極大的恐慌飒炎,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笆豁,死亡現(xiàn)場離奇詭異郎汪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)闯狱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門煞赢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人哄孤,你說我怎么就攤上這事照筑。” “怎么了瘦陈?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵凝危,是天一觀的道長。 經(jīng)常有香客問我晨逝,道長蛾默,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任捉貌,我火速辦了婚禮支鸡,結(jié)果婚禮上冬念,老公的妹妹穿的比我還像新娘。我一直安慰自己牧挣,他們只是感情好急前,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瀑构,像睡著了一般裆针。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上检碗,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天据块,我揣著相機(jī)與錄音码邻,去河邊找鬼折剃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛像屋,可吹牛的內(nèi)容都是我干的怕犁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼己莺,長吁一口氣:“原來是場噩夢啊……” “哼奏甫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起凌受,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤阵子,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后胜蛉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挠进,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年誊册,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了领突。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡案怯,死狀恐怖君旦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嘲碱,我是刑警寧澤金砍,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站麦锯,受9級特大地震影響恕稠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜离咐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一谱俭、第九天 我趴在偏房一處隱蔽的房頂上張望奉件。 院中可真熱鬧,春花似錦昆著、人聲如沸县貌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽煤痕。三九已至,卻和暖如春接谨,著一層夾襖步出監(jiān)牢的瞬間摆碉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工脓豪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留巷帝,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓扫夜,卻偏偏與公主長得像楞泼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子笤闯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容