部分的商用虛擬機中辐怕,Java程序最初是通過解釋器進行解釋執(zhí)行的,當虛擬機發(fā)現(xiàn)某個方法或代碼塊的運行特別頻繁時疫稿,就會把這些代碼認定為“熱點代碼”(Hot Spot Code)张弛。為了提高熱點代碼的運行效率,在運行時意推,虛擬機將會把這些代碼編譯成與本地平臺相關(guān)的機器碼豆瘫,并進行各種層次的優(yōu)化,完成這個任務的編譯器稱為即時編譯器(Just In Time Compiler菊值,簡稱JIT編譯器)外驱。
即時編譯器并不是虛擬機的必要組成部分,JVM規(guī)范中并沒有規(guī)定JVM必須要有即時編譯器的存在腻窒,更沒有限定或指導即時編譯器應該如何實現(xiàn)略步。但是,即時編譯器編譯性能的好壞定页、代碼優(yōu)化程度的高低卻是衡量一款商用虛擬機優(yōu)秀與否的最關(guān)鍵的指標之一趟薄,它也是虛擬機中最核心且最能體現(xiàn)虛擬機技術(shù)水平的部分。
HotSpot虛擬機的即時編譯器
我們要了解HotSpot虛擬機內(nèi)的即時編譯器的運作過程典徊,同時杭煎,還要解決以下幾個問題:
- 為何HotSpot虛擬機要使用解釋器與編譯器并存的架構(gòu)?
- 為何HotSpot虛擬機要實現(xiàn)兩個不同的即時編譯器卒落?
- 程序何時使用解釋器執(zhí)行羡铲?何時使用編譯器執(zhí)行?
- 哪些程序代碼會被編譯為本地代碼儡毕?如何編譯為本地代碼也切?
- 如何從外部觀察即時編譯器的編譯過程和編譯結(jié)果
解釋器和編譯器
解釋器和編譯器各有兩個優(yōu)勢:
- 當程序需要迅速啟動和執(zhí)行的時候,解釋器首先發(fā)揮作用腰湾,省去編譯的時間雷恃,立即執(zhí)行。
- 在程序運行后费坊,隨著時間的推移倒槐,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后附井,可以獲取更高的執(zhí)行效率讨越。
- 當程序運行環(huán)境中內(nèi)存資源限制較大(如部分嵌入式系統(tǒng)中),可以使用解釋執(zhí)行節(jié)約內(nèi)存永毅,繁殖可以使用編譯執(zhí)行來提升效率把跨。
- 同時,解釋器還可以作為編譯器激進優(yōu)化時的一個逃生門沼死,讓編譯器根據(jù)效率選擇一些大多數(shù)時候都能提升運行速度的優(yōu)化手段着逐,當激進優(yōu)化的假設不成立,如加載了新類之后類型繼承結(jié)構(gòu)出現(xiàn)變化、出現(xiàn)罕見陷阱時可以通過逆優(yōu)化退回到解釋狀態(tài)繼續(xù)執(zhí)行滨嘱。
因此峰鄙,在整體虛擬機執(zhí)行架構(gòu)中浸间,解釋器和編譯器經(jīng)常配合工作太雨。
HotSpot虛擬機中內(nèi)置了兩個即時編譯器囊扳,分別是Client Compiler和Server Compiler兜看,或者簡稱C1編譯器和C2編譯器。目前主流的HotSpot虛擬機中细移,默認采用解釋器與一個編譯器直接配合的方式工作,程序使用哪個編譯器雪侥,取決于虛擬機運行的模式,HotSpot虛擬機會根據(jù)自身版本與宿主機器的硬件性能自動選擇運行模式精绎,用戶也可以使用“-client”或“-server”參數(shù)去強制指定虛擬機運行在Client模式或Server模式速缨。
無論采用的編譯器是Client Compiler還是Server Compiler,解釋器與編譯器搭配使用的方式在虛擬機中稱為混合模式代乃,用戶可以使用參數(shù)“-Xint”強制虛擬機運行于“解釋模式”旬牲,這時編譯器完全不介入工作,全部代碼都使用解釋方式執(zhí)行搁吓。另外原茅,也可以使用參數(shù)“-Xcomp”強制虛擬機運行于“編譯模式”,這時將優(yōu)先采用編譯方式執(zhí)行程序堕仔,但是解釋器仍然要在編譯無法進行的時候進入執(zhí)行過程员咽,可以通過虛擬機的“-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)
由于即時編譯器編譯本地代碼需要占用程序運行時間,要編譯出優(yōu)化程度更高的代碼贮预,所花費的時間可能更長:而且要編譯出優(yōu)化程度更高的代碼贝室,解釋器可能還要替編譯器收集性能監(jiān)控信息,這對解釋執(zhí)行的速度也有影響仿吞。為了在程序啟動相應速度和運行效率之間達到最佳平衡滑频,HotSpot虛擬機還會逐漸啟用分層編譯(Tiered Compilation)的策略,分層編譯的概念在JDK1.6時期出現(xiàn)唤冈,后來一直處于改進階段峡迷,最終在JDK1.7的Server模式虛擬機中作為默認編譯策略被開啟。分層編譯根據(jù)編譯器編譯、優(yōu)化的規(guī)模與耗時绘搞,劃分出不同的編譯層次彤避,其中包括:
- 第0層,程序解釋執(zhí)行夯辖,解釋器不開啟性能監(jiān)控功能(Profiling)琉预,可觸發(fā)第1層編譯。
- 第1層蒿褂,也成為C1編譯,將字節(jié)碼編譯為本地代碼啄栓,進行簡單近速、可靠的優(yōu)化削葱,如有必要加入性能監(jiān)控的邏輯佩耳。
- 第2層(或2層以上)干厚,也稱為C2編譯蛮瞄,也是將字節(jié)碼編譯為本地代碼挂捅,但是會啟用一些編譯耗時較長的優(yōu)化闲先,甚至會根據(jù)性能監(jiān)控信息進行一些不可靠的僅僅有話。
實施分層編譯后斥季,Client Compiler和Server Compiler將會同時工作,許多代碼都可能會被多次編譯谤专,用Client Compiler獲取更高的編譯速度置侍,用Server Compiler來獲取更好的編譯質(zhì)量墅垮,在解釋執(zhí)行的時候也無須再承擔收集性能監(jiān)控信息的任務。
編譯對象與觸發(fā)條件
在運行過程中會即時編譯的“熱點代碼”有兩類螟够,
- 被多次調(diào)用的方法
- 被多次執(zhí)行的循環(huán)體
前者好理解妓笙,一個方法被調(diào)用的多了,方法內(nèi)部代碼執(zhí)行的次數(shù)自然就多辈赋,它成為“熱點代碼”是理所當然的钥屈。而后者則是為了解決一個方法只被調(diào)用一次或少量幾次篷就,但是方法體內(nèi)部存在循環(huán)次數(shù)較多的循環(huán)體的問題竭业,這樣循環(huán)體的代碼也被重復執(zhí)行多次,因此這些代碼也應該認為是“熱點代碼”鼎姐。
對于第一種情況饭尝,由于是由方法調(diào)用觸發(fā)的編譯钥平,因此編譯器理所當然的會以整個方法作為編譯對象涉瘾,這種編譯也是虛擬機中標準的JIT編譯方式立叛。而對于后一種情況秘蛇,盡管編譯動作是由循環(huán)體所觸發(fā)的赁还,但編譯器依然會以整個方法作為編譯對象艘策。
這種編譯方式因為編譯發(fā)生在方法執(zhí)行過程之中,因此形象地稱之為棧上替換(On Stack Replacement斑举,簡稱OSR編譯富玷,即方法棧幀還在棧上赎懦,方法就被替換了)励两。
在上面的文字描述中傅瞻,無論是“多次執(zhí)行的方法”嗅骄,還是“多次執(zhí)行的代碼塊”,所謂“多次”都不是一個具體屏积、嚴謹?shù)挠谜Z,那么到底多少次才算“多次”呢?還有一個問題蜻直,就是虛擬機如何統(tǒng)計一個方法或一段代碼被執(zhí)行過多少次呢括眠?解決了這兩個問題捞烟,也就回答了即時編譯被觸發(fā)的條件当船。
判斷一段代碼是不是熱點代碼苍息,這樣的行為稱為熱點探測竞思,其實進行熱點探測并不一定要知道方法具體被調(diào)用了多少次,目前熱點探測判定的方法有兩種:
- 基于采樣的熱點cancel:采用這種方法的虛擬機會周期性的檢查各個線程的棧頂课梳,如果發(fā)現(xiàn)某個(或某些)方法經(jīng)常出現(xiàn)在棧頂,那這個方法就是熱點方法∑基于采樣的熱點探測的好處是實現(xiàn)簡單挫望、高效,還可以很容易的獲得方法調(diào)用關(guān)系(將調(diào)用堆棧展開即可)狂窑,缺點是很難精確地確認一個方法的熱度媳板,容易因為受到線程阻塞或別的外界因素的影響而擾亂熱點探測。
- 基于計數(shù)器的熱點探測:采用這種方式的虛擬機會為每個方法甚至代碼塊建立計數(shù)器泉哈,統(tǒng)計方法的執(zhí)行次數(shù)蛉幸,如果執(zhí)行次數(shù)超過一定的閾值就任務它是熱點方法。這種統(tǒng)計方法實現(xiàn)起來麻煩一些丛晦,需要為每個方法建立并維護計數(shù)器奕纫,而且不能直接獲取到方法的調(diào)用關(guān)系,但是它的統(tǒng)計結(jié)果相對來說更加精確嚴謹。
在HotSpot虛擬機中使用的是第二種:計數(shù)器熱點探測剪决。因為它為每個方法都準備了兩類計數(shù)器:方法調(diào)用計數(shù)器(Invocation Counter)和回邊計數(shù)器(Back Edge Counter)担锤。在確定虛擬機運行參數(shù)的前提下累舷,這兩個計數(shù)器都有一個確定的閾值,當計數(shù)器閾值溢出了拍鲤,就會觸發(fā)JIT編譯。
方法調(diào)用計數(shù)器
顧名思義帘营,這個計數(shù)器就用于統(tǒng)計方法被調(diào)用的次數(shù),它的默認閾值在Client模式下是1500次,在Server模式下是10000次,這個閾值可以通過虛擬機參數(shù)-XX:CompileThreshold來人為設定。當一個方法調(diào)用時厚柳,會先檢查該方法是否在被JIT編譯過的版本毁靶,如果存在,則優(yōu)先使用編譯后的本地代碼來執(zhí)行梆靖。如果不存在已被編譯過的版本,則將此方法的調(diào)用計數(shù)器+1舆乔,然后判斷方法調(diào)用計數(shù)器與會變計數(shù)器之和是否超過方法調(diào)用計數(shù)器的閾值鳞上。如果已超越閾值香璃,那么將會向即時編譯器提交一個該方法的代碼編譯請求星澳。
如果不做任何設置笆檀,執(zhí)行引擎并不會同步等待編譯請求完成枷遂,而是繼續(xù)進入解釋器按照屆時方式執(zhí)行字節(jié)碼网沾,直到提交的請求被編譯器編譯完成。當編譯工作完成之后摄狱,這個方法的調(diào)用入口地址就會被系統(tǒng)自動改寫成新的脓诡,下一次調(diào)用該方法時就會使用已編譯的版本。
如果不做任何限制媒役,方法調(diào)用計數(shù)器統(tǒng)計的并不是方法被調(diào)用的絕對次數(shù)祝谚,而是一個相對的執(zhí)行頻率,即一段時間之內(nèi)方法被調(diào)用的次數(shù)酣衷。當超過一定的時間限度交惯,如果方法的調(diào)用次數(shù)仍然不足以讓它提交給即時編譯器編譯,那這個方法的調(diào)用計數(shù)器會被減少一半,這個過程稱為方法調(diào)用計數(shù)器熱度的衰減席爽,而這段時間就稱為此方法統(tǒng)計的半衰期(Counter Half Life Time)意荤。進行熱度衰減的動作是在虛擬機進行垃圾收集時順帶進行的,可以使用虛擬機參數(shù)-XX:-UseCounterDecay來關(guān)閉熱度衰減只锻,讓方法計數(shù)器統(tǒng)計方法調(diào)用的絕對次數(shù)玖像,這樣,只要系統(tǒng)運行時間足夠長炬藤,絕大多數(shù)方法都會被編譯成本地代碼御铃。另外,可以使用-XX:CounterHalfLifeTime參數(shù)設置半衰周期的時間沈矿,時間是秒上真。
回邊計數(shù)器
雖然HotSpot虛擬機也提供了一個類似于方法調(diào)用計數(shù)器閾值-XX:CompileThreshold的參數(shù)-XX:BackEdgeThreshold供用戶設置,但是當前的虛擬機實際上并未使用此參數(shù)羹膳,因此我們需要設置另外一個參數(shù)-XX:OnStackReplacePercentage來間接調(diào)整回邊計數(shù)器的閾值睡互,其計算公式如下
- 虛擬機運行在Client模式下,回邊計數(shù)器閾值計算公式為:方法調(diào)用計數(shù)器閾值×OSR比率÷100陵像。其中就珠,OSR比率(OnStackReplacePercentage)默認值為933,如果都去默認值醒颖,那Client模式虛擬機的回邊計數(shù)器的閾值就是13995妻怎。
- 虛擬機運行在Server模式下,回邊計數(shù)器閾值的計算公式為:方法計數(shù)器閾值×(OSR比率-解釋器監(jiān)控比率)÷100.其中OSR比率默認值為140泞歉,解釋器監(jiān)控比率(InterpreterProfilePercentage)默認值為33逼侦,如果都是默認值,Server模式虛擬機回邊計數(shù)器的閾值為10700腰耙。
當解釋器遇到一條回邊指令時榛丢,會先查找要執(zhí)行的代碼片段是否有已經(jīng)編譯好的版本,如果有挺庞,它將會優(yōu)先執(zhí)行已經(jīng)編譯的代碼晰赞,否則就把回邊計數(shù)器的值加1,然后判斷方法調(diào)用計數(shù)器與回邊計數(shù)器的閾值选侨。當超過閾值的時候掖鱼,將會提交一個OSR請求,并且把回邊計數(shù)器的值降低一些援制,以便在解釋器中執(zhí)行循環(huán)锨用,等待編譯器輸出編譯結(jié)果,整個過程如下所示:
與方法計數(shù)器不同隘谣,回邊計數(shù)器沒有計數(shù)熱度衰減的過程,因此這個計數(shù)器統(tǒng)計的就是該方法循環(huán)執(zhí)行的絕對次數(shù)。當方法溢出的時候寻歧,它還會把方法計數(shù)器的值調(diào)整到溢出狀態(tài)掌栅,這樣下次進入該方法的時候就會執(zhí)行標準編譯過程。
需要注意的是码泛,上面的兩張流程圖展示的都僅僅是ClientVM的即時編譯方式猾封,對于ServerVM來說,執(zhí)行情況會比上面的描述更復雜一些噪珊。
編譯過程
在默認設置下晌缘,無論是方法調(diào)用產(chǎn)生的即時編譯請求,還是OSR編譯請求痢站,虛擬機在代碼編譯器還未完成之前磷箕,都任然按照解釋方式繼續(xù)執(zhí)行,而編譯動作則在后臺的編譯線程中進行阵难,用戶可以通過參數(shù)-XX:-BackgroundCompilation來禁止后臺編譯岳枷,在禁止后臺編譯后,一旦達到JIT的編譯條件呜叫,執(zhí)行線程向虛擬機提交編譯器輸出的本地代碼空繁。
在后臺執(zhí)行編譯的過程中,Server Compiler和Client Compiler兩個編譯器的編譯過程是不一樣的朱庆。對于ClientCompiler來說盛泡,它是一個簡單快速的三段式編譯器你,主要的關(guān)注點在于局部性的優(yōu)化娱颊,而放棄了許多耗時較長的全局優(yōu)化手段傲诵。
在第一階段,一個平臺獨立的前端將字節(jié)碼構(gòu)造成一種高級中間代碼表示(High-Level Intermediate Representation维蒙, HIR)掰吕。HIR使用的靜態(tài)單分派(Static Single Assignment, SSA)的形式來代表代碼值颅痊,這可以使得一些在HIR的構(gòu)造過程之中和之后進行的優(yōu)化動作更容易實現(xiàn)殖熟。在此之前編譯器會在字節(jié)碼上完成一部分基礎優(yōu)化,如方法內(nèi)聯(lián)斑响、常量傳播等優(yōu)化將會在字節(jié)碼被構(gòu)造成HIR之前完成菱属。
在第二階段,一個平臺相關(guān)的后端從HIR中產(chǎn)生低級中間代碼表示(Low-Level Intermediate Representation舰罚, LIR)纽门,在此之前會在HIR上完成另外一些優(yōu)化,如空值檢查消除营罢、范圍檢查消除等赏陵,以便讓HIR達到更高效的代碼表示形式饼齿。
最后階段是在平臺相關(guān)的后端使用線性掃描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窺孔(Peephole)優(yōu)化蝙搔,然后產(chǎn)生機器代碼缕溉。Client Compiler的大致執(zhí)行過程如圖:
而ServerCompiler則是專門面向服務端的典型應用并為服務端的性能配置特別調(diào)整過的編譯器,也是一個充分優(yōu)化過的高級編譯器吃型,幾乎能達到GNU C++編譯器使用-O2參數(shù)時的優(yōu)化輕度证鸥,它會執(zhí)行所有經(jīng)典的優(yōu)化動作,如無用代碼消除(Dead Code Elimination)勤晚、循環(huán)展開(Loop Unrolling)枉层、循環(huán)表達式外提(Loop Expression Hoisting)、消除公共子表達式(Common Subexpression Elimination)赐写、常量傳播(Constant Propagation)鸟蜡、基本塊重排序(Basic Block Reordering)等,還會實施一些與Java語言特性密切相關(guān)的優(yōu)化技術(shù)血淌,如范圍檢查消除(Range Check Elimination)矩欠、空值檢查消除(Null Check Elimination,不過并非所有的空值檢查消除都是依賴編譯器優(yōu)化的悠夯,有一些是在代碼過程中自動優(yōu)化了)等癌淮。另外,如守護內(nèi)聯(lián)(Guarded Inlining)沦补、分支頻率預測(Branch Frequency Prediction)等乳蓄。
Server Compiler的寄存器分配器是一個全局圖著色分配器,它可以充分利用某些處理架構(gòu)(如RISC)上的大寄存器集合夕膀。以即時編譯器的標準來看虚倒,Server Compiler無疑是比較緩慢的,但它的編譯速度依然遠遠超過傳統(tǒng)的靜態(tài)優(yōu)化編譯器产舞,而且它相對于Client Compiler編譯輸出的代碼質(zhì)量有所提高魂奥,可以減少本地代碼的執(zhí)行時間,從而抵消了額外的編譯時間開銷易猫,所以也有很多非服務端的應用選擇使用Server模式的虛擬機運行耻煤。
公共子表達式消除
公共子表達式小數(shù)是一個普遍存在于各種編譯器的經(jīng)典優(yōu)化技術(shù),它的含義是:如果一個表達式E已經(jīng)計算過了准颓,并且從先前的計算到現(xiàn)在E中所有變量的值都沒有發(fā)生變化哈蝇,那么E的這次出現(xiàn)就稱為了公共子表達式。對于這種表達式攘已,沒有必要花時間再對它計算炮赦,只需要直接用錢買計算過的表達式結(jié)果代替E就行了。如果這種優(yōu)化僅限用程序的基本塊內(nèi)样勃,便稱為局部公共子表達式消除(Local Common Subexpression Elimination)吠勘,如果這種優(yōu)化的范圍覆蓋了多個基本塊性芬,那就稱為全局公共子表達式消除(Global Common Subexpression Elimination)。舉個例子說明它的優(yōu)化過程看幼,假設存在如下代碼:
int a = 1, b = 2, c = 3;
int d = (c * b) * 12 + a + (a + b * c);
如果直接交給Javac編譯器則不會進行任何優(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 //計算b*c
9: bipush 12 //推入12
11: imul //計算(b*c)*12
12: iload_1 //a
13: iadd //計算(b*c)*12+a
14: iload_1 //a
15: iload_2 //b
16: iload_3 //c
17: imul //計算(b*c)
18: iadd //計算a+b*c
19: iadd //計算(b*c)*12+a+(a+b*c)
20: istore 4
22: return
當這段代碼進入到虛擬機即時編譯器后,它將進行如下優(yōu)化:編譯器檢查到“c*b”與“b*c”是一樣的表達式诵姜,而且在執(zhí)行期間b與c的值是不變的。因此這條表達式就可被視為:
int d = E * 12 + a + (a + E);
這時編譯器還可能進行了另外一種優(yōu)化:代數(shù)化簡(這取決于哪種虛擬機的編譯器以及具體的上下文而定)搏熄,把表達式變?yōu)椋?/p>
int d = E * 13 + a * 2;
數(shù)組邊界檢查消除
數(shù)組邊界檢查消除(Array Bounds Checking Elimination)是即時編譯器中的一項語言相關(guān)的經(jīng)典優(yōu)化技術(shù)棚唆。我們知道Java語言是一門動態(tài)安全的語言,對數(shù)組的讀寫訪問不像C心例、C++那樣在本質(zhì)上是裸指針操作宵凌。如果有一個數(shù)組foo[],在Java語言中訪問數(shù)組元素foo[i]的時候系統(tǒng)將會自動進行上下界的范圍檢查止后,即檢查i必須滿足i>=0&&i<foo.length這個條件瞎惫,否則將拋出一個運行時異常:java.lang.ArrayIndexOutOfBoundsException。這對軟件開發(fā)者來說是一件很好的事情译株,即使程序員沒有專門編寫防御代碼瓜喇,也可以避免大部分的溢出攻擊。但是對于虛擬機的執(zhí)行子系統(tǒng)來說歉糜,每次數(shù)組元素的讀寫都帶有一次隱含的條件判定操作乘寒,對于擁有大量數(shù)組訪問的程序代碼,這是一種性能負擔匪补。
無論如何伞辛,數(shù)組邊界檢查肯定是必須做的,但數(shù)組邊界檢查是不是必須在運行期間一次不漏的檢查夯缺。例如:數(shù)組下標是一個常量蚤氏,如foo[3],只要在編譯期根據(jù)數(shù)據(jù)流分析來確定foo.length的值踊兜,并判斷下標“3”沒有越界竿滨,執(zhí)行的時候就無須判斷了。更加常見的情況是數(shù)組訪問發(fā)生在循環(huán)之后润文,并且使用循環(huán)變量來進行數(shù)組訪問姐呐,如果編譯器只要通過數(shù)據(jù)流分析就可以判定循環(huán)變量的取值范圍永遠在[0, foo.length)之內(nèi),那么在整個循環(huán)中就可以把數(shù)組的上下界檢查消除典蝌,這可以節(jié)省很多次的條件判斷操作曙砂。
將這個數(shù)組邊界檢查的例子放在更高的角度來看,大量的安全檢查令編寫Java程序比編寫C/C++容易的多骏掀,如數(shù)組越界會得到ArrayIndexOutOfBoundsException異常鸠澈,空指針訪問會得到NullPointException柱告,除數(shù)為0會得到ArithmeticException等,在C/C++程序中出現(xiàn)類似的問題笑陈,一不小心就會出現(xiàn)Segment Fault信號或者Window編程中常見的“xxx內(nèi)存不能為Read/Write”之類的提示际度,處理不好程序就直接崩潰退出了。但這些安全檢查也導致了相同的程序涵妥,Java要比C/C++做更多的事情乖菱,這些事情就成為一種隱式的開銷,如果處理不好蓬网,就很可能成為一個Java語言比C/C++更慢的因素窒所。要消除這些隱式開銷,除了如數(shù)組邊界檢查優(yōu)化的這種盡可能把運行期檢查提到編譯期完成的思路之外帆锋,另外還有一種思路--隱式異常處理吵取,Java中控指針檢查和算數(shù)運算中除數(shù)為0的檢查都是這種思路。例如锯厢,程序中訪問一個對象foo的某個屬性value皮官,那么以Java偽代碼表示虛擬機訪問的過程如下。
if(foo != null) {
return foo.value;
} else {
throw new NullPointException();
}
在隱式異常優(yōu)化后实辑,虛擬機會把上面的偽代碼轉(zhuǎn)變成如下操作
try {
return foo.value;
} catch(segment_fault) {
uncommon_trap();
}
虛擬機會注冊一個Segment Fault信號的異常處理器捺氢,這樣當foo不為空的時候,對value的訪問是不會額外消耗一次對foo判空的開銷的徙菠。代價就是當foo真的為空的時候讯沈,必須轉(zhuǎn)入到異常處理器中恢復并拋出NullPointException異常,這個過程必須從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài)中處理婿奔,結(jié)束后再回到用戶態(tài)缺狠,速度遠比一次判空檢查慢。當foo極少為空的時候萍摊,隱式異常優(yōu)化是值得的挤茄,但假如foo經(jīng)常為空,這樣的優(yōu)化反而會讓程序更慢冰木,還好HotSpot會根據(jù)運行期間收集到的Profile信息自動選擇最優(yōu)方案穷劈。
方法內(nèi)聯(lián)
方法內(nèi)聯(lián)是編譯器最重要的優(yōu)化手段之一,除了消除方法調(diào)用的成本之外踊沸,它更重要的意義是為其他優(yōu)化手段建立良好的基礎歇终。如下所示:
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);
}
事實上,testInline方法內(nèi)部全部都是無用的代碼逼龟,如果不做內(nèi)聯(lián)评凝,后續(xù)繼續(xù)進行了無用代碼的消除優(yōu)化,也無法發(fā)現(xiàn)任何“Dead Code”腺律,因為如果分開看奕短,foo和testInline里面的操作都可能是有意義的宜肉。
方法內(nèi)聯(lián)的優(yōu)化看起來很簡單,不過是把目標方法的代碼“復制”到發(fā)起調(diào)用的方法之中翎碑,避免發(fā)生真是的方法調(diào)用而谬返。但實際上JVM中的內(nèi)聯(lián)過程遠遠沒有那么簡單,因為如果不是即時編譯器做了一些努力日杈,按照經(jīng)典編譯原理的優(yōu)化理論遣铝,大多數(shù)的Java方法都無法進行內(nèi)聯(lián)。
無法內(nèi)聯(lián)的原因是:只有使用invokespecial指令調(diào)用的私有方法达椰、實例構(gòu)造器翰蠢、父類方法以及使用invokestatic指令進行調(diào)用的靜態(tài)方法才是在編譯期進行解析的,除了上述4種方法之外啰劲,其他的Java方法調(diào)用都需要在運行時進行方法接收者的多態(tài)選擇,并且都有可能存在多于一個版本的方法接收者檀何,簡而言之蝇裤,Java語言中默認的實例方法是虛方法。
對于一個虛方法频鉴,編譯期做內(nèi)聯(lián)的時候根本無法確定應該使用哪個方法版本栓辜。由于Java語言提倡使用面向?qū)ο螅瑸榱私鉀Q虛方法的內(nèi)聯(lián)問題垛孔,JVM設計團隊想了很多辦法藕甩,首先是引入了一個名為“類型繼承關(guān)系分析”(Class Hierarchy Analysis,CHA)的技術(shù)周荐,這是一種基于整個應用程序的類型分析技術(shù)狭莱,它用于確定在目前已加載的類中,某個接口是否有多于一種的實現(xiàn)概作,某個類是否存在子類腋妙、子類是否為抽象類等信息。
編譯器在內(nèi)聯(lián)的時候讯榕,如果方法是非虛方法骤素,那么直接進行內(nèi)聯(lián)就可以了,這個時候的內(nèi)聯(lián)是由穩(wěn)定前提保障的愚屁。如果遇到虛方法济竹,則會向CHA查詢此方法在當前程序下是否有多個目標版本可供選擇,如果查詢結(jié)果只有一個版本霎槐,那也可以進行內(nèi)聯(lián)送浊,不過這種內(nèi)聯(lián)就屬于激進優(yōu)化,需要預留一個逃生門(Guard條件不成立時的SlowPath)栽燕,稱為守護內(nèi)聯(lián)(Guarded Inlining)罕袋。如果程序的后續(xù)執(zhí)行過程中改淑,虛擬機一致沒有加載到會令這個方法的接收者的繼承關(guān)系發(fā)生變化的類,那這個內(nèi)聯(lián)優(yōu)化的代碼就可以一直使用下去浴讯。但如果加載了導致繼承關(guān)系變化的類朵夏,那就需要拋棄已經(jīng)編譯的代碼,退回到解釋狀態(tài)執(zhí)行榆纽,或者重新編譯仰猖。
所以說,在許多情況下虛擬機進行的內(nèi)聯(lián)都是一種激進優(yōu)化奈籽,激進優(yōu)化的手段在高性能的商用虛擬機中很常見饥侵,除了內(nèi)聯(lián)之外,對于出現(xiàn)概率很幸缕痢(通過經(jīng)驗數(shù)據(jù)或解釋器收集到的性能監(jiān)控信息確定概率大絮锷)的隱式異常、使用概率很小的分支等都可以被激進優(yōu)化“移出”狼忱,如果真的出現(xiàn)了小概率事件膨疏,這時才會從“逃生門”回到解釋狀態(tài)重新執(zhí)行。
Java與C/C++的編譯器對比
Java與C/C++的編譯器對比實際上代表了最經(jīng)典的即時編譯器與靜態(tài)編譯器的對比钻弄,很大程度上也決定了Java與C/C++的性能對比的結(jié)果佃却,因為無論C/C++還是Java代碼,最終編譯之后被機器執(zhí)行的都是本地機器碼窘俺,哪種語言的性能更高饲帅,除了它們自身的API庫實現(xiàn)得好壞之外,其余的比較都是一場“拼編譯器”和“拼輸出代碼質(zhì)量”的游戲瘤泪。當然灶泵,這種比較也是剔除了開發(fā)效率的片面對比,語言間孰優(yōu)孰劣均芽、誰慢誰快的問題都是很難有結(jié)果的爭論丘逸。
Java虛擬機的即時編譯器與C/C++的靜態(tài)優(yōu)化編譯器相比,可能會由于下列原因?qū)е螺敵龅谋镜卮a有一些劣勢掀宋。
- 第一深纲,因為即時編譯器運行占用的是用戶程序的運行時間,具有很大的時間夜里劲妙,它能提供的優(yōu)化手段也嚴重受制于編譯成本湃鹊。如果編譯速度不能達到要求,那用戶將在啟動程序或程序的某部分察覺到重大延遲镣奋,這點使得即時編譯器不敢隨便引入大規(guī)模的優(yōu)化技術(shù)币呵,而編譯的時間內(nèi)成本在靜態(tài)優(yōu)化編譯器中并不是主要的關(guān)注點。
- 第二,Java語言是動態(tài)的類型安全語言余赢,這就意味著需要由虛擬機來確保程序不會違反程序語言語義或訪問非結(jié)構(gòu)化內(nèi)存芯义。從實現(xiàn)層面上看,這就意味著虛擬機必須頻繁的進行動態(tài)檢查妻柒,如實例方法訪問時檢查空指針扛拨、數(shù)組元素訪問時檢查上下界范圍、類型轉(zhuǎn)換時檢查繼承關(guān)系等举塔。這對這列程序代碼沒有明確寫出檢查行為绑警,盡管編譯器會努力進行優(yōu)化,但是總體上仍然要消耗不少的運行時間央渣。
- 第三计盒,Java語言中雖然沒有virtual關(guān)鍵字,但是使用虛方法的頻率遠遠大于C/C++語言芽丹,這意味著運行時堆方法接收者進行多態(tài)選擇的頻率要遠遠大于C/C++語言北启,也意味著即時編譯器在運行一些優(yōu)化時難度要遠大于C/C++的靜態(tài)優(yōu)化編譯器。
- 第四拔第,Java語言是可以動態(tài)擴展的語言暖庄,運行時加載的新類可能改變程序類型的繼承關(guān)系,這使得很多全局的優(yōu)化都難以進行楼肪,因為編譯器無法看見程序的全貌,許多全局的優(yōu)化措施都只能以激進優(yōu)化的方式來完成惹悄,編譯器不得不時刻注意并隨著類型的變化而在運行時撤銷或重新進行一些優(yōu)化春叫。
- 第五,Java語言中對象的內(nèi)存分配都是堆上進行的泣港,只有方法中的局部變量才能在棧上分配暂殖。而C/C++的對象則有多種內(nèi)存分配方式,既可能在堆上分配当纱,又可能在棧上分配呛每,如果可以在棧上分配線程私有的對象,將減輕內(nèi)存回收的壓力坡氯。另外晨横,C/C++中主要由用戶程序員來回收分配的內(nèi)存,這局不存在無用對象篩選的過程箫柳,因此運行效率上也比垃圾回收機制要高手形。
Java語言的這些性能上的劣勢都是為了換取開發(fā)效率的優(yōu)勢而付出的代建,動態(tài)安全悯恍、動態(tài)擴展库糠、垃圾回收這些特性都為Java語言的開發(fā)效率做出了很大的貢獻。