Java運(yùn)行期優(yōu)化

部分的商用虛擬機(jī)中,Java程序最初是通過解釋器進(jìn)行解釋執(zhí)行的,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊的運(yùn)行特別頻繁時(shí),就會(huì)把這些代碼認(rèn)定為“熱點(diǎn)代碼”(Hot Spot Code)。為了提高熱點(diǎn)代碼的運(yùn)行效率,在運(yùn)行時(shí),虛擬機(jī)將會(huì)把這些代碼編譯成與本地平臺(tái)相關(guān)的機(jī)器碼,并進(jìn)行各種層次的優(yōu)化,完成這個(gè)任務(wù)的編譯器稱為即時(shí)編譯器(Just In Time Compiler,簡(jiǎn)稱JIT編譯器)。

即時(shí)編譯器并不是虛擬機(jī)的必要組成部分,JVM規(guī)范中并沒有規(guī)定JVM必須要有即時(shí)編譯器的存在,更沒有限定或指導(dǎo)即時(shí)編譯器應(yīng)該如何實(shí)現(xiàn)。但是,即時(shí)編譯器編譯性能的好壞、代碼優(yōu)化程度的高低卻是衡量一款商用虛擬機(jī)優(yōu)秀與否的最關(guān)鍵的指標(biāo)之一,它也是虛擬機(jī)中最核心且最能體現(xiàn)虛擬機(jī)技術(shù)水平的部分。

HotSpot虛擬機(jī)的即時(shí)編譯器

我們要了解HotSpot虛擬機(jī)內(nèi)的即時(shí)編譯器的運(yùn)作過程,同時(shí),還要解決以下幾個(gè)問題:

  • 為何HotSpot虛擬機(jī)要使用解釋器與編譯器并存的架構(gòu)?
  • 為何HotSpot虛擬機(jī)要實(shí)現(xiàn)兩個(gè)不同的即時(shí)編譯器?
  • 程序何時(shí)使用解釋器執(zhí)行?何時(shí)使用編譯器執(zhí)行?
  • 哪些程序代碼會(huì)被編譯為本地代碼?如何編譯為本地代碼?
  • 如何從外部觀察即時(shí)編譯器的編譯過程和編譯結(jié)果

解釋器和編譯器

解釋器和編譯器各有兩個(gè)優(yōu)勢(shì):

  • 當(dāng)程序需要迅速啟動(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í)行來提升效率。
  • 同時(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)罕見陷阱時(shí)可以通過逆優(yōu)化退回到解釋狀態(tài)繼續(xù)執(zhí)行。

因此,在整體虛擬機(jī)執(zhí)行架構(gòu)中,解釋器和編譯器經(jīng)常配合工作。

HotSpot虛擬機(jī)中內(nèi)置了兩個(gè)即時(shí)編譯器,分別是Client Compiler和Server Compiler,或者簡(jiǎn)稱C1編譯器和C2編譯器。目前主流的HotSpot虛擬機(jī)中,默認(rèn)采用解釋器與一個(gè)編譯器直接配合的方式工作,程序使用哪個(gè)編譯器,取決于虛擬機(jī)運(yùn)行的模式,HotSpot虛擬機(jī)會(huì)根據(jù)自身版本與宿主機(jī)器的硬件性能自動(dòng)選擇運(yùn)行模式,用戶也可以使用“-client”或“-server”參數(shù)去強(qiáng)制指定虛擬機(jī)運(yùn)行在Client模式或Server模式。

無論采用的編譯器是Client Compiler還是Server Compiler,解釋器與編譯器搭配使用的方式在虛擬機(jī)中稱為混合模式,用戶可以使用參數(shù)“-Xint”強(qiáng)制虛擬機(jī)運(yùn)行于“解釋模式”,這時(shí)編譯器完全不介入工作,全部代碼都使用解釋方式執(zhí)行。另外,也可以使用參數(shù)“-Xcomp”強(qiáng)制虛擬機(jī)運(yùn)行于“編譯模式”,這時(shí)將優(yōu)先采用編譯方式執(zhí)行程序,但是解釋器仍然要在編譯無法進(jìn)行的時(shí)候進(jìn)入執(zhí)行過程,可以通過虛擬機(jī)的“-version”命令的輸出結(jié)果看出這3種模式

C:\>java -version
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b18)
Java HotSpot(TM) 64-Bit Server VM (build 25.66-b18, mixed mode)

C:\>java -Xint -version
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b18)
Java HotSpot(TM) 64-Bit Server VM (build 25.66-b18, interpreted mode)

C:\>java -Xcomp -version
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b18)
Java HotSpot(TM) 64-Bit Server VM (build 25.66-b18, compiled mode)

由于即時(shí)編譯器編譯本地代碼需要占用程序運(yùn)行時(shí)間,要編譯出優(yōu)化程度更高的代碼,所花費(fèi)的時(shí)間可能更長(zhǎng):而且要編譯出優(yōu)化程度更高的代碼,解釋器可能還要替編譯器收集性能監(jiān)控信息,這對(duì)解釋執(zhí)行的速度也有影響。為了在程序啟動(dòng)相應(yīng)速度和運(yùn)行效率之間達(dá)到最佳平衡,HotSpot虛擬機(jī)還會(huì)逐漸啟用分層編譯(Tiered Compilation)的策略,分層編譯的概念在JDK1.6時(shí)期出現(xiàn),后來一直處于改進(jìn)階段,最終在JDK1.7的Server模式虛擬機(jī)中作為默認(rèn)編譯策略被開啟。分層編譯根據(jù)編譯器編譯、優(yōu)化的規(guī)模與耗時(shí),劃分出不同的編譯層次,其中包括:

  • 第0層,程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控功能(Profiling),可觸發(fā)第1層編譯。
  • 第1層,也成為C1編譯,將字節(jié)碼編譯為本地代碼,進(jìn)行簡(jiǎn)單、可靠的優(yōu)化,如有必要加入性能監(jiān)控的邏輯。
  • 第2層(或2層以上),也稱為C2編譯,也是將字節(jié)碼編譯為本地代碼,但是會(huì)啟用一些編譯耗時(shí)較長(zhǎng)的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進(jìn)行一些不可靠的僅僅有話。

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

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

在運(yùn)行過程中會(huì)即時(shí)編譯的“熱點(diǎn)代碼”有兩類,

  • 被多次調(diào)用的方法
  • 被多次執(zhí)行的循環(huán)體

前者好理解,一個(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)代碼”。

對(duì)于第一種情況,由于是由方法調(diào)用觸發(fā)的編譯,因此編譯器理所當(dāng)然的會(huì)以整個(gè)方法作為編譯對(duì)象,這種編譯也是虛擬機(jī)中標(biāo)準(zhǔn)的JIT編譯方式。而對(duì)于后一種情況,盡管編譯動(dòng)作是由循環(huán)體所觸發(fā)的,但編譯器依然會(huì)以整個(gè)方法作為編譯對(duì)象。
這種編譯方式因?yàn)榫幾g發(fā)生在方法執(zhí)行過程之中,因此形象地稱之為棧上替換(On Stack Replacement,簡(jiǎn)稱OSR編譯,即方法棧幀還在棧上,方法就被替換了)。

在上面的文字描述中,無論是“多次執(zhí)行的方法”,還是“多次執(zhí)行的代碼塊”,所謂“多次”都不是一個(gè)具體、嚴(yán)謹(jǐn)?shù)挠谜Z,那么到底多少次才算“多次”呢?還有一個(gè)問題,就是虛擬機(jī)如何統(tǒng)計(jì)一個(gè)方法或一段代碼被執(zhí)行過多少次呢?解決了這兩個(gè)問題,也就回答了即時(shí)編譯被觸發(fā)的條件。

判斷一段代碼是不是熱點(diǎn)代碼,這樣的行為稱為熱點(diǎn)探測(cè),其實(shí)進(jìn)行熱點(diǎn)探測(cè)并不一定要知道方法具體被調(diào)用了多少次,目前熱點(diǎn)探測(cè)判定的方法有兩種:

  • 基于采樣的熱點(diǎn)cancel:采用這種方法的虛擬機(jī)會(huì)周期性的檢查各個(gè)線程的棧頂,如果發(fā)現(xiàn)某個(gè)(或某些)方法經(jīng)常出現(xiàn)在棧頂,那這個(gè)方法就是熱點(diǎn)方法。基于采樣的熱點(diǎn)探測(cè)的好處是實(shí)現(xiàn)簡(jiǎn)單、高效,還可以很容易的獲得方法調(diào)用關(guān)系(將調(diào)用堆棧展開即可),缺點(diǎn)是很難精確地確認(rèn)一個(gè)方法的熱度,容易因?yàn)槭艿骄€程阻塞或別的外界因素的影響而擾亂熱點(diǎn)探測(cè)。
  • 基于計(jì)數(shù)器的熱點(diǎn)探測(cè):采用這種方式的虛擬機(jī)會(huì)為每個(gè)方法甚至代碼塊建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過一定的閾值就任務(wù)它是熱點(diǎn)方法。這種統(tǒng)計(jì)方法實(shí)現(xiàn)起來麻煩一些,需要為每個(gè)方法建立并維護(hù)計(jì)數(shù)器,而且不能直接獲取到方法的調(diào)用關(guān)系,但是它的統(tǒng)計(jì)結(jié)果相對(duì)來說更加精確嚴(yán)謹(jǐn)。

在HotSpot虛擬機(jī)中使用的是第二種:計(jì)數(shù)器熱點(diǎn)探測(cè)。因?yà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ù)器閾值溢出了,就會(huì)觸發(fā)JIT編譯。

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

顧名思義,這個(gè)計(jì)數(shù)器就用于統(tǒng)計(jì)方法被調(diào)用的次數(shù),它的默認(rèn)閾值在Client模式下是1500次,在Server模式下是10000次,這個(gè)閾值可以通過虛擬機(jī)參數(shù)-XX:CompileThreshold來人為設(shè)定。當(dāng)一個(gè)方法調(diào)用時(shí),會(huì)先檢查該方法是否在被JIT編譯過的版本,如果存在,則優(yōu)先使用編譯后的本地代碼來執(zhí)行。如果不存在已被編譯過的版本,則將此方法的調(diào)用計(jì)數(shù)器+1,然后判斷方法調(diào)用計(jì)數(shù)器與會(huì)變計(jì)數(shù)器之和是否超過方法調(diào)用計(jì)數(shù)器的閾值。如果已超越閾值,那么將會(huì)向即時(shí)編譯器提交一個(gè)該方法的代碼編譯請(qǐng)求。

如果不做任何設(shè)置,執(zhí)行引擎并不會(huì)同步等待編譯請(qǐng)求完成,而是繼續(xù)進(jìn)入解釋器按照屆時(shí)方式執(zhí)行字節(jié)碼,直到提交的請(qǐng)求被編譯器編譯完成。當(dāng)編譯工作完成之后,這個(gè)方法的調(diào)用入口地址就會(huì)被系統(tǒng)自動(dòng)改寫成新的,下一次調(diào)用該方法時(shí)就會(huì)使用已編譯的版本。

方法計(jì)數(shù)器觸發(fā)即時(shí)編譯

如果不做任何限制,方法調(diào)用計(jì)數(shù)器統(tǒng)計(jì)的并不是方法被調(diào)用的絕對(duì)次數(shù),而是一個(gè)相對(duì)的執(zhí)行頻率,即一段時(shí)間之內(nèi)方法被調(diào)用的次數(shù)。當(dāng)超過一定的時(shí)間限度,如果方法的調(diào)用次數(shù)仍然不足以讓它提交給即時(shí)編譯器編譯,那這個(gè)方法的調(diào)用計(jì)數(shù)器會(huì)被減少一半,這個(gè)過程稱為方法調(diào)用計(jì)數(shù)器熱度的衰減,而這段時(shí)間就稱為此方法統(tǒng)計(jì)的半衰期(Counter Half Life Time)。進(jìn)行熱度衰減的動(dòng)作是在虛擬機(jī)進(jìn)行垃圾收集時(shí)順帶進(jìn)行的,可以使用虛擬機(jī)參數(shù)-XX:-UseCounterDecay來關(guān)閉熱度衰減,讓方法計(jì)數(shù)器統(tǒng)計(jì)方法調(diào)用的絕對(duì)次數(shù),這樣,只要系統(tǒng)運(yùn)行時(shí)間足夠長(zhǎng),絕大多數(shù)方法都會(huì)被編譯成本地代碼。另外,可以使用-XX:CounterHalfLifeTime參數(shù)設(shè)置半衰周期的時(shí)間,時(shí)間是秒。

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

雖然HotSpot虛擬機(jī)也提供了一個(gè)類似于方法調(diào)用計(jì)數(shù)器閾值-XX:CompileThreshold的參數(shù)-XX:BackEdgeThreshold供用戶設(shè)置,但是當(dāng)前的虛擬機(jī)實(shí)際上并未使用此參數(shù),因此我們需要設(shè)置另外一個(gè)參數(shù)-XX:OnStackReplacePercentage來間接調(diào)整回邊計(jì)數(shù)器的閾值,其計(jì)算公式如下

  • 虛擬機(jī)運(yùn)行在Client模式下,回邊計(jì)數(shù)器閾值計(jì)算公式為:方法調(diào)用計(jì)數(shù)器閾值×OSR比率÷100。其中,OSR比率(OnStackReplacePercentage)默認(rèn)值為933,如果都去默認(rèn)值,那Client模式虛擬機(jī)的回邊計(jì)數(shù)器的閾值就是13995。
  • 虛擬機(jī)運(yùn)行在Server模式下,回邊計(jì)數(shù)器閾值的計(jì)算公式為:方法計(jì)數(shù)器閾值×(OSR比率-解釋器監(jiān)控比率)÷100.其中OSR比率默認(rèn)值為140,解釋器監(jiān)控比率(InterpreterProfilePercentage)默認(rèn)值為33,如果都是默認(rèn)值,Server模式虛擬機(jī)回邊計(jì)數(shù)器的閾值為10700。

當(dāng)解釋器遇到一條回邊指令時(shí),會(huì)先查找要執(zhí)行的代碼片段是否有已經(jīng)編譯好的版本,如果有,它將會(huì)優(yōu)先執(zhí)行已經(jīng)編譯的代碼,否則就把回邊計(jì)數(shù)器的值加1,然后判斷方法調(diào)用計(jì)數(shù)器與回邊計(jì)數(shù)器的閾值。當(dāng)超過閾值的時(shí)候,將會(huì)提交一個(gè)OSR請(qǐng)求,并且把回邊計(jì)數(shù)器的值降低一些,以便在解釋器中執(zhí)行循環(huán),等待編譯器輸出編譯結(jié)果,整個(gè)過程如下所示:

回邊計(jì)數(shù)器觸發(fā)即時(shí)編譯

與方法計(jì)數(shù)器不同,回邊計(jì)數(shù)器沒有計(jì)數(shù)熱度衰減的過程,因此這個(gè)計(jì)數(shù)器統(tǒng)計(jì)的就是該方法循環(huán)執(zhí)行的絕對(duì)次數(shù)。當(dāng)方法溢出的時(shí)候,它還會(huì)把方法計(jì)數(shù)器的值調(diào)整到溢出狀態(tài),這樣下次進(jìn)入該方法的時(shí)候就會(huì)執(zhí)行標(biāo)準(zhǔn)編譯過程。

需要注意的是,上面的兩張流程圖展示的都僅僅是ClientVM的即時(shí)編譯方式,對(duì)于ServerVM來說,執(zhí)行情況會(huì)比上面的描述更復(fù)雜一些。

編譯過程

在默認(rèn)設(shè)置下,無論是方法調(diào)用產(chǎn)生的即時(shí)編譯請(qǐng)求,還是OSR編譯請(qǐng)求,虛擬機(jī)在代碼編譯器還未完成之前,都任然按照解釋方式繼續(xù)執(zhí)行,而編譯動(dòng)作則在后臺(tái)的編譯線程中進(jìn)行,用戶可以通過參數(shù)-XX:-BackgroundCompilation來禁止后臺(tái)編譯,在禁止后臺(tái)編譯后,一旦達(dá)到JIT的編譯條件,執(zhí)行線程向虛擬機(jī)提交編譯器輸出的本地代碼。

在后臺(tái)執(zhí)行編譯的過程中,Server Compiler和Client Compiler兩個(gè)編譯器的編譯過程是不一樣的。對(duì)于ClientCompiler來說,它是一個(gè)簡(jiǎn)單快速的三段式編譯器你,主要的關(guān)注點(diǎn)在于局部性的優(yōu)化,而放棄了許多耗時(shí)較長(zhǎng)的全局優(yōu)化手段。

在第一階段,一個(gè)平臺(tái)獨(dú)立的前端將字節(jié)碼構(gòu)造成一種高級(jí)中間代碼表示(High-Level Intermediate Representation, HIR)。HIR使用的靜態(tài)單分派(Static Single Assignment, SSA)的形式來代表代碼值,這可以使得一些在HIR的構(gòu)造過程之中和之后進(jìn)行的優(yōu)化動(dòng)作更容易實(shí)現(xiàn)。在此之前編譯器會(huì)在字節(jié)碼上完成一部分基礎(chǔ)優(yōu)化,如方法內(nèi)聯(lián)、常量傳播等優(yōu)化將會(huì)在字節(jié)碼被構(gòu)造成HIR之前完成。

在第二階段,一個(gè)平臺(tái)相關(guān)的后端從HIR中產(chǎn)生低級(jí)中間代碼表示(Low-Level Intermediate Representation, LIR),在此之前會(huì)在HIR上完成另外一些優(yōu)化,如空值檢查消除、范圍檢查消除等,以便讓HIR達(dá)到更高效的代碼表示形式。

最后階段是在平臺(tái)相關(guān)的后端使用線性掃描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窺孔(Peephole)優(yōu)化,然后產(chǎn)生機(jī)器代碼。Client Compiler的大致執(zhí)行過程如圖:

Client Compiler架構(gòu)

而ServerCompiler則是專門面向服務(wù)端的典型應(yīng)用并為服務(wù)端的性能配置特別調(diào)整過的編譯器,也是一個(gè)充分優(yōu)化過的高級(jí)編譯器,幾乎能達(dá)到GNU C++編譯器使用-O2參數(shù)時(shí)的優(yōu)化輕度,它會(huì)執(zhí)行所有經(jīng)典的優(yōu)化動(dòng)作,如無用代碼消除(Dead Code Elimination)、循環(huán)展開(Loop Unrolling)、循環(huán)表達(dá)式外提(Loop Expression Hoisting)、消除公共子表達(dá)式(Common Subexpression Elimination)、常量傳播(Constant Propagation)、基本塊重排序(Basic Block Reordering)等,還會(huì)實(shí)施一些與Java語言特性密切相關(guān)的優(yōu)化技術(shù),如范圍檢查消除(Range Check Elimination)、空值檢查消除(Null Check Elimination,不過并非所有的空值檢查消除都是依賴編譯器優(yōu)化的,有一些是在代碼過程中自動(dòng)優(yōu)化了)等。另外,如守護(hù)內(nèi)聯(lián)(Guarded Inlining)、分支頻率預(yù)測(cè)(Branch Frequency Prediction)等。

Server Compiler的寄存器分配器是一個(gè)全局圖著色分配器,它可以充分利用某些處理架構(gòu)(如RISC)上的大寄存器集合。以即時(shí)編譯器的標(biāo)準(zhǔn)來看,Server Compiler無疑是比較緩慢的,但它的編譯速度依然遠(yuǎn)遠(yuǎn)超過傳統(tǒng)的靜態(tài)優(yōu)化編譯器,而且它相對(duì)于Client Compiler編譯輸出的代碼質(zhì)量有所提高,可以減少本地代碼的執(zhí)行時(shí)間,從而抵消了額外的編譯時(shí)間開銷,所以也有很多非服務(wù)端的應(yīng)用選擇使用Server模式的虛擬機(jī)運(yùn)行。

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

公共子表達(dá)式小數(shù)是一個(gè)普遍存在于各種編譯器的經(jīng)典優(yōu)化技術(shù),它的含義是:如果一個(gè)表達(dá)式E已經(jīng)計(jì)算過了,并且從先前的計(jì)算到現(xiàn)在E中所有變量的值都沒有發(fā)生變化,那么E的這次出現(xiàn)就稱為了公共子表達(dá)式。對(duì)于這種表達(dá)式,沒有必要花時(shí)間再對(duì)它計(jì)算,只需要直接用錢買計(jì)算過的表達(dá)式結(jié)果代替E就行了。如果這種優(yōu)化僅限用程序的基本塊內(nèi),便稱為局部公共子表達(dá)式消除(Local Common Subexpression Elimination),如果這種優(yōu)化的范圍覆蓋了多個(gè)基本塊,那就稱為全局公共子表達(dá)式消除(Global Common Subexpression Elimination)。舉個(gè)例子說明它的優(yōu)化過程,假設(shè)存在如下代碼:

int a = 1, b = 2, c = 3;
int d = (c * b) * 12 + a + (a + b * c);

如果直接交給Javac編譯器則不會(huì)進(jìn)行任何優(yōu)化,字節(jié)碼如下:

         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iconst_3
         5: istore_3
         6: iload_3
         7: iload_2       //b
         8: imul          //計(jì)算b*c
         9: bipush 12     //推入12
        11: imul          //計(jì)算(b*c)*12
        12: iload_1       //a
        13: iadd          //計(jì)算(b*c)*12+a
        14: iload_1       //a
        15: iload_2       //b
        16: iload_3       //c
        17: imul          //計(jì)算(b*c)
        18: iadd          //計(jì)算a+b*c
        19: iadd          //計(jì)算(b*c)*12+a+(a+b*c)
        20: istore 4
        22: return

當(dāng)這段代碼進(jìn)入到虛擬機(jī)即時(shí)編譯器后,它將進(jìn)行如下優(yōu)化:編譯器檢查到“c*b”與“b*c”是一樣的表達(dá)式,而且在執(zhí)行期間b與c的值是不變的。因此這條表達(dá)式就可被視為:

int d = E * 12 + a + (a + E);

這時(shí)編譯器還可能進(jìn)行了另外一種優(yōu)化:代數(shù)化簡(jiǎn)(這取決于哪種虛擬機(jī)的編譯器以及具體的上下文而定),把表達(dá)式變?yōu)椋?/p>

int d = E * 13 + a * 2;

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

數(shù)組邊界檢查消除(Array Bounds Checking Elimination)是即時(shí)編譯器中的一項(xiàng)語言相關(guān)的經(jīng)典優(yōu)化技術(shù)。我們知道Java語言是一門動(dòng)態(tài)安全的語言,對(duì)數(shù)組的讀寫訪問不像C、C++那樣在本質(zhì)上是裸指針操作。如果有一個(gè)數(shù)組foo[],在Java語言中訪問數(shù)組元素foo[i]的時(shí)候系統(tǒng)將會(huì)自動(dòng)進(jìn)行上下界的范圍檢查,即檢查i必須滿足i>=0&&i<foo.length這個(gè)條件,否則將拋出一個(gè)運(yùn)行時(shí)異常:java.lang.ArrayIndexOutOfBoundsException。這對(duì)軟件開發(fā)者來說是一件很好的事情,即使程序員沒有專門編寫防御代碼,也可以避免大部分的溢出攻擊。但是對(duì)于虛擬機(jī)的執(zhí)行子系統(tǒng)來說,每次數(shù)組元素的讀寫都帶有一次隱含的條件判定操作,對(duì)于擁有大量數(shù)組訪問的程序代碼,這是一種性能負(fù)擔(dān)。

無論如何,數(shù)組邊界檢查肯定是必須做的,但數(shù)組邊界檢查是不是必須在運(yùn)行期間一次不漏的檢查。例如:數(shù)組下標(biāo)是一個(gè)常量,如foo[3],只要在編譯期根據(jù)數(shù)據(jù)流分析來確定foo.length的值,并判斷下標(biāo)“3”沒有越界,執(zhí)行的時(shí)候就無須判斷了。更加常見的情況是數(shù)組訪問發(fā)生在循環(huán)之后,并且使用循環(huán)變量來進(jìn)行數(shù)組訪問,如果編譯器只要通過數(shù)據(jù)流分析就可以判定循環(huán)變量的取值范圍永遠(yuǎn)在[0, foo.length)之內(nèi),那么在整個(gè)循環(huán)中就可以把數(shù)組的上下界檢查消除,這可以節(jié)省很多次的條件判斷操作。

將這個(gè)數(shù)組邊界檢查的例子放在更高的角度來看,大量的安全檢查令編寫Java程序比編寫C/C++容易的多,如數(shù)組越界會(huì)得到ArrayIndexOutOfBoundsException異常,空指針訪問會(huì)得到NullPointException,除數(shù)為0會(huì)得到ArithmeticException等,在C/C++程序中出現(xiàn)類似的問題,一不小心就會(huì)出現(xiàn)Segment Fault信號(hào)或者Window編程中常見的“xxx內(nèi)存不能為Read/Write”之類的提示,處理不好程序就直接崩潰退出了。但這些安全檢查也導(dǎo)致了相同的程序,Java要比C/C++做更多的事情,這些事情就成為一種隱式的開銷,如果處理不好,就很可能成為一個(gè)Java語言比C/C++更慢的因素。要消除這些隱式開銷,除了如數(shù)組邊界檢查優(yōu)化的這種盡可能把運(yùn)行期檢查提到編譯期完成的思路之外,另外還有一種思路--隱式異常處理,Java中控指針檢查和算數(shù)運(yùn)算中除數(shù)為0的檢查都是這種思路。例如,程序中訪問一個(gè)對(duì)象foo的某個(gè)屬性value,那么以Java偽代碼表示虛擬機(jī)訪問的過程如下。

if(foo != null) {
    return foo.value;
} else {
    throw new NullPointException();
}

在隱式異常優(yōu)化后,虛擬機(jī)會(huì)把上面的偽代碼轉(zhuǎn)變成如下操作

try {
    return foo.value;
} catch(segment_fault) {
    uncommon_trap();
}

虛擬機(jī)會(huì)注冊(cè)一個(gè)Segment Fault信號(hào)的異常處理器,這樣當(dāng)foo不為空的時(shí)候,對(duì)value的訪問是不會(huì)額外消耗一次對(duì)foo判空的開銷的。代價(jià)就是當(dāng)foo真的為空的時(shí)候,必須轉(zhuǎn)入到異常處理器中恢復(fù)并拋出NullPointException異常,這個(gè)過程必須從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài)中處理,結(jié)束后再回到用戶態(tài),速度遠(yuǎn)比一次判空檢查慢。當(dāng)foo極少為空的時(shí)候,隱式異常優(yōu)化是值得的,但假如foo經(jīng)常為空,這樣的優(yōu)化反而會(huì)讓程序更慢,還好HotSpot會(huì)根據(jù)運(yùn)行期間收集到的Profile信息自動(dòng)選擇最優(yōu)方案。

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

方法內(nèi)聯(lián)是編譯器最重要的優(yōu)化手段之一,除了消除方法調(diào)用的成本之外,它更重要的意義是為其他優(yōu)化手段建立良好的基礎(chǔ)。如下所示:

public static void foo(Object obj){
    if(obj != null){
        System.out.println("do something");
    }
}

public static void testInline(String[] args){
    Object obj = null;
    foo(obj);
}

事實(shí)上,testInline方法內(nèi)部全部都是無用的代碼,如果不做內(nèi)聯(lián),后續(xù)繼續(xù)進(jìn)行了無用代碼的消除優(yōu)化,也無法發(fā)現(xiàn)任何“Dead Code”,因?yàn)槿绻珠_看,foo和testInline里面的操作都可能是有意義的。

方法內(nèi)聯(lián)的優(yōu)化看起來很簡(jiǎn)單,不過是把目標(biāo)方法的代碼“復(fù)制”到發(fā)起調(diào)用的方法之中,避免發(fā)生真是的方法調(diào)用而。但實(shí)際上JVM中的內(nèi)聯(lián)過程遠(yuǎn)遠(yuǎn)沒有那么簡(jiǎn)單,因?yàn)槿绻皇羌磿r(shí)編譯器做了一些努力,按照經(jīng)典編譯原理的優(yōu)化理論,大多數(shù)的Java方法都無法進(jìn)行內(nèi)聯(lián)。

無法內(nèi)聯(lián)的原因是:只有使用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è)版本的方法接收者,簡(jiǎn)而言之,Java語言中默認(rèn)的實(shí)例方法是虛方法。

對(duì)于一個(gè)虛方法,編譯期做內(nèi)聯(lián)的時(shí)候根本無法確定應(yīng)該使用哪個(gè)方法版本。由于Java語言提倡使用面向?qū)ο?,為了解決虛方法的內(nèi)聯(lián)問題,JVM設(shè)計(jì)團(tuán)隊(duì)想了很多辦法,首先是引入了一個(gè)名為“類型繼承關(guān)系分析”(Class Hierarchy Analysis,CHA)的技術(shù),這是一種基于整個(gè)應(yīng)用程序的類型分析技術(shù),它用于確定在目前已加載的類中,某個(gè)接口是否有多于一種的實(shí)現(xiàn),某個(gè)類是否存在子類、子類是否為抽象類等信息。

編譯器在內(nèi)聯(lián)的時(shí)候,如果方法是非虛方法,那么直接進(jìn)行內(nèi)聯(lián)就可以了,這個(gè)時(shí)候的內(nèi)聯(lián)是由穩(wěn)定前提保障的。如果遇到虛方法,則會(huì)向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í)的SlowPath),稱為守護(hù)內(nèi)聯(lián)(Guarded Inlining)。如果程序的后續(xù)執(zhí)行過程中,虛擬機(jī)一致沒有加載到會(huì)令這個(gè)方法的接收者的繼承關(guān)系發(fā)生變化的類,那這個(gè)內(nèi)聯(lián)優(yōu)化的代碼就可以一直使用下去。但如果加載了導(dǎo)致繼承關(guān)系變化的類,那就需要拋棄已經(jīng)編譯的代碼,退回到解釋狀態(tài)執(zhí)行,或者重新編譯。

所以說,在許多情況下虛擬機(jī)進(jìn)行的內(nèi)聯(lián)都是一種激進(jìn)優(yōu)化,激進(jìn)優(yōu)化的手段在高性能的商用虛擬機(jī)中很常見,除了內(nèi)聯(lián)之外,對(duì)于出現(xiàn)概率很?。ㄍㄟ^經(jīng)驗(yàn)數(shù)據(jù)或解釋器收集到的性能監(jiān)控信息確定概率大小)的隱式異常、使用概率很小的分支等都可以被激進(jìn)優(yōu)化“移出”,如果真的出現(xiàn)了小概率事件,這時(shí)才會(huì)從“逃生門”回到解釋狀態(tài)重新執(zhí)行。

Java與C/C++的編譯器對(duì)比

Java與C/C++的編譯器對(duì)比實(shí)際上代表了最經(jīng)典的即時(shí)編譯器與靜態(tài)編譯器的對(duì)比,很大程度上也決定了Java與C/C++的性能對(duì)比的結(jié)果,因?yàn)闊o論C/C++還是Java代碼,最終編譯之后被機(jī)器執(zhí)行的都是本地機(jī)器碼,哪種語言的性能更高,除了它們自身的API庫實(shí)現(xiàn)得好壞之外,其余的比較都是一場(chǎng)“拼編譯器”和“拼輸出代碼質(zhì)量”的游戲。當(dāng)然,這種比較也是剔除了開發(fā)效率的片面對(duì)比,語言間孰優(yōu)孰劣、誰慢誰快的問題都是很難有結(jié)果的爭(zhēng)論。

Java虛擬機(jī)的即時(shí)編譯器與C/C++的靜態(tài)優(yōu)化編譯器相比,可能會(huì)由于下列原因?qū)е螺敵龅谋镜卮a有一些劣勢(shì)。

  • 第一,因?yàn)榧磿r(shí)編譯器運(yùn)行占用的是用戶程序的運(yùn)行時(shí)間,具有很大的時(shí)間夜里,它能提供的優(yōu)化手段也嚴(yán)重受制于編譯成本。如果編譯速度不能達(dá)到要求,那用戶將在啟動(dòng)程序或程序的某部分察覺到重大延遲,這點(diǎn)使得即時(shí)編譯器不敢隨便引入大規(guī)模的優(yōu)化技術(shù),而編譯的時(shí)間內(nèi)成本在靜態(tài)優(yōu)化編譯器中并不是主要的關(guān)注點(diǎn)。
  • 第二,Java語言是動(dòng)態(tài)的類型安全語言,這就意味著需要由虛擬機(jī)來確保程序不會(huì)違反程序語言語義或訪問非結(jié)構(gòu)化內(nèi)存。從實(shí)現(xiàn)層面上看,這就意味著虛擬機(jī)必須頻繁的進(jìn)行動(dòng)態(tài)檢查,如實(shí)例方法訪問時(shí)檢查空指針、數(shù)組元素訪問時(shí)檢查上下界范圍、類型轉(zhuǎn)換時(shí)檢查繼承關(guān)系等。這對(duì)這列程序代碼沒有明確寫出檢查行為,盡管編譯器會(huì)努力進(jìn)行優(yōu)化,但是總體上仍然要消耗不少的運(yùn)行時(shí)間。
  • 第三,Java語言中雖然沒有virtual關(guān)鍵字,但是使用虛方法的頻率遠(yuǎn)遠(yuǎn)大于C/C++語言,這意味著運(yùn)行時(shí)堆方法接收者進(jìn)行多態(tài)選擇的頻率要遠(yuǎn)遠(yuǎn)大于C/C++語言,也意味著即時(shí)編譯器在運(yùn)行一些優(yōu)化時(shí)難度要遠(yuǎn)大于C/C++的靜態(tài)優(yōu)化編譯器。
  • 第四,Java語言是可以動(dòng)態(tài)擴(kuò)展的語言,運(yùn)行時(shí)加載的新類可能改變程序類型的繼承關(guān)系,這使得很多全局的優(yōu)化都難以進(jìn)行,因?yàn)榫幾g器無法看見程序的全貌,許多全局的優(yōu)化措施都只能以激進(jìn)優(yōu)化的方式來完成,編譯器不得不時(shí)刻注意并隨著類型的變化而在運(yùn)行時(shí)撤銷或重新進(jìn)行一些優(yōu)化。
  • 第五,Java語言中對(duì)象的內(nèi)存分配都是堆上進(jìn)行的,只有方法中的局部變量才能在棧上分配。而C/C++的對(duì)象則有多種內(nèi)存分配方式,既可能在堆上分配,又可能在棧上分配,如果可以在棧上分配線程私有的對(duì)象,將減輕內(nèi)存回收的壓力。另外,C/C++中主要由用戶程序員來回收分配的內(nèi)存,這局不存在無用對(duì)象篩選的過程,因此運(yùn)行效率上也比垃圾回收機(jī)制要高。

Java語言的這些性能上的劣勢(shì)都是為了換取開發(fā)效率的優(yōu)勢(shì)而付出的代建,動(dòng)態(tài)安全、動(dòng)態(tài)擴(kuò)展、垃圾回收這些特性都為Java語言的開發(fā)效率做出了很大的貢獻(xiàn)。

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

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

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