Java程序最初是通過解釋器(Interpreter)進行解釋執(zhí)行的净嘀,當虛擬機發(fā)現(xiàn)某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定為“熱點代碼”(Hot Spot Code)侠讯。為了提高熱點代碼的執(zhí)行效率挖藏,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關(guān)的機器碼厢漩,并進行各種層次的優(yōu)化膜眠,完成這個任務(wù)的編譯器稱為即時編譯器(Just In Time Compiler,下文中簡稱JIT編譯器)溜嗜。
由于Java虛擬機規(guī)范沒有具體的約束規(guī)則去限制即時編譯器應(yīng)該如何實現(xiàn)宵膨,所以這部分功能完全是與虛擬機具體實現(xiàn)(Implementation Specific)相關(guān)的內(nèi)容,如無特殊說明炸宵,本文提及的編譯器辟躏、即時編譯器都是指HotSpot虛擬機內(nèi)的即時編譯器,虛擬機也是特指HotSpot虛擬機土全。
一捎琐、HotSpot虛擬機內(nèi)的即時編譯器
1、解釋器與編譯器
盡管并不是所有的Java虛擬機都采用解釋器與編譯器并存的架構(gòu)涯曲,但許多主流的商用虛擬機野哭,如HotSpot、J9等幻件,都同時包含解釋器與編譯器拨黔。
解釋器與編譯器兩者各有優(yōu)勢:當程序需要迅速啟動和執(zhí)行的時候,解釋器可以首先發(fā)揮作用绰沥,省去編譯的時間篱蝇,立即執(zhí)行。在程序運行后徽曲,隨著時間的推移零截,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后秃臣,可以獲取更高的執(zhí)行效率涧衙。當程序運行環(huán)境中內(nèi)存資源限制較大(如部分嵌入式系統(tǒng)中),可以使用解釋執(zhí)行節(jié)約內(nèi)存奥此,反之可以使用編譯執(zhí)行來提升效率弧哎。
HotSpot虛擬機中內(nèi)置了兩個即時編譯器,分別稱為Client Compiler
和Server Compiler
稚虎,或者簡稱為C1編譯器
和C2編譯器
(也叫Opto編譯器)撤嫩。目前主流的HotSpot虛擬機(中,默認采用解釋器與其中一個編譯器直接配合的方式工作蠢终,程序使用哪個編譯器序攘,取決于虛擬機運行的模式茴她,HotSpot虛擬機會根據(jù)自身版本與宿主機器的硬件性能自動選擇運行模式,用戶也可以使用"-client"或"-server"參數(shù)去強制指定虛擬機運行在Client模式或Server模式程奠。
無論采用的編譯器是Client Compiler還是Server Compiler丈牢,解釋器與編譯器搭配使用的方式在虛擬機中稱為“混合模式”(Mixed Mode),用戶可以使用參數(shù)"-Xint"強制虛擬機運行于“解釋模式”(Interpreted Mode)梦染,這時編譯器完全不介入工作赡麦,全部代碼都使用解釋方式執(zhí)行朴皆。另外帕识,也可以使用參數(shù)"-Xcomp"強制虛擬機運行于“編譯模式”(Compiled Mode),這時將優(yōu)先采用編譯方式執(zhí)行程序遂铡,但是解釋器仍然要在編譯無法進行的情況下介入執(zhí)行過程肮疗。
由于即時編譯器編譯本地代碼需要占用程序運行時間,要編譯出優(yōu)化程度更高的代碼扒接,所花費的時間可能更長伪货;而且想要編譯出優(yōu)化程度更高的代碼,解釋器可能還要替編譯器收集性能監(jiān)控信息钾怔,這對解釋執(zhí)行的速度也有影響碱呼。為了在程序啟動響應(yīng)速度與運行效率之間達到最佳平衡,HotSpot虛擬機還會逐漸啟用分層編譯(Tiered Compilation)的策略宗侦。
分層編譯根據(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)控信息進行一些不可靠的激進優(yōu)化因篇。
2泞辐、編譯對象與觸發(fā)條件
在運行過程中會被即時編譯器編譯的“熱點代碼”有兩類笔横,即:
被多次調(diào)用的方法。
-
被多次執(zhí)行的循環(huán)體咐吼。
前者很好理解吹缔,一個方法被調(diào)用得多了,方法體內(nèi)代碼執(zhí)行的次數(shù)自然就多锯茄,它成為“熱點代碼”是理所當然的厢塘。而后者則是為了解決一個方法只被調(diào)用過一次或少量的幾次,但是方法體內(nèi)部存在循環(huán)次數(shù)較多的循環(huán)體的問題肌幽,這樣循環(huán)體的代碼也被重復(fù)執(zhí)行多次
晚碾,因此這些代碼也應(yīng)該認為是“熱點代碼”。
對于第一種情況喂急,由于是由方法調(diào)用觸發(fā)的編譯格嘁,因此編譯器理所當然地會以整個方法作為編譯對象,這種編譯也是虛擬機中標準的JIT編譯方式廊移。而對于后一種情況糕簿,盡管編譯動作是由循環(huán)體所觸發(fā)的,但編譯器依然會以整個方法(而不是單獨的循環(huán)體)作為編譯對象狡孔。這種編譯方式因為編譯發(fā)生在方法執(zhí)行過程之中懂诗,因此形象地稱之為棧上替換
(On Stack Replacement,簡稱為OSR編譯苗膝,即方法棧幀還在棧上殃恒,方法就被替換了)。
判斷一段代碼是不是熱點代碼荚醒,是不是需要觸發(fā)即時編譯芋类,這樣的行為稱為熱點探測(Hot Spot Detection),其實進行熱點探測并不一定要知道方法具體被調(diào)用了多少次界阁,目前主要的熱點探測判定方式有兩種:
-
基于采樣的熱點探測(Sample Based Hot Spot Detection)
:采用這種方法的虛擬機會周期性地檢查各個線程的棧頂侯繁,如果發(fā)現(xiàn)某個(或某些)方法經(jīng)常出現(xiàn)在棧頂,那這個方法就是“熱點方法”泡躯≈梗基于采樣的熱點探測的好處是實現(xiàn)簡單、高效较剃,還可以很容易地獲取方法調(diào)用關(guān)系(將調(diào)用堆棧展開即可)咕别,缺點是很難精確地確認一個方法的熱度,容易因為受到線程阻塞或別的外界因素的影響而擾亂熱點探測写穴。 -
基于計數(shù)器的熱點探測(Counter Based Hot Spot Detection)
:采用這種方法的虛擬機會為每個方法(甚至是代碼塊)建立計數(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)。
3勾怒、編譯過程
在默認設(shè)置下婆排,無論是方法調(diào)用產(chǎn)生的即時編譯請求,還是OSR編譯請求笔链,虛擬機在代碼編譯器還未完成之前段只,都仍然將按照解釋方式繼續(xù)執(zhí)行,而編譯動作則在后臺的編譯線程中進行卡乾。
那么在后臺執(zhí)行編譯的過程中翼悴,編譯器做了什么事情呢?Server Compiler和Client Compiler兩個編譯器的編譯過程是不一樣的幔妨。對于Client Compiler來說,它是一個簡單快速的三段式編譯器谍椅,主要的關(guān)注點在于局部性的優(yōu)化误堡,而放棄了許多耗時較長的全局優(yōu)化手段傲诵。
而Server Compiler則是專門面向服務(wù)端的典型應(yīng)用并為服務(wù)端的性能配置特別調(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)化了)等。另外杭措,還可能根據(jù)解釋器或Client Compiler提供的性能監(jiān)控信息费什,進行一些不穩(wěn)定的激進優(yōu)化,如守護內(nèi)聯(lián)(Guarded Inlining)
手素、分支頻率預(yù)測(Branch Frequency Prediction)
等鸳址。
4赘那、查看及分析即時編譯結(jié)果
測試代碼:
public static final int NUM=15000;
public static int doubleValue(int i){
//這個空循環(huán)用于后面演示JIT代碼優(yōu)化過程
for(int j=0氯质;j<100000募舟;j++);
return i*2闻察;
}
public static long calcSum(){
long sum=0拱礁;
for(int i=1;i<=100辕漂;i++){
sum+=doubleValue(i)呢灶;
return sum;
}
public static void main(String[]args){
for(int i=0钉嘹;i<NUM鸯乃;i++){
calcSum();
}
}
}
首先運行這段代碼跋涣,并且確認這段代碼是否觸發(fā)了即時編譯缨睡,要知道某個方法是否被編譯過,可以使用參數(shù)-XX:+PrintCompilation要求虛擬機在即時編譯時將被編譯成本地代碼的方法名稱打印出來陈辱。
被即時編譯的代碼
VM option'+PrintCompilation'
310 1 java.lang.String:charAt(33 bytes)
329 2 org.fenixsoft.jit.Test:calcSum(26 bytes)
329 3 org.fenixsoft.jit.Test:doubleValue(4 bytes)
332 1%org.fenixsoft.jit.Test:main@5(20 bytes)
輸出的確認信息中可以確認main()奖年、calcSum()和doubleValue()方法已經(jīng)被編譯,我們還可以加上參數(shù)-XX:+PrintInlining要求虛擬機輸出方法內(nèi)聯(lián)信息
“VM option'+PrintCompilation'
VM option'+PrintInlining'
273 1 java.lang.String:charAt(33 bytes)
291 2 org.fenixsoft.jit.Test:calcSum(26 bytes)
@9 org.fenixsoft.jit.Test:doubleValue inline(hot)
294 3 org.fenixsoft.jit.Test:doubleValue(4 bytes)
295 1%org.fenixsoft.jit.Test:main@5(20 bytes)
@5 org.fenixsoft.jit.Test:calcSum inline(hot)
@9 org.fenixsoft.jit.Test:doubleValue inline(hot)
可以看到方法doubleValue()被內(nèi)聯(lián)編譯到calcSum()中沛贪,而calcSum()又被內(nèi)聯(lián)編譯到方法main()中陋守,所以虛擬機再次執(zhí)行main()方法的時候,calcSum()和doubleValue()方法都不會再被調(diào)用利赋,它們的代碼邏輯都被直接內(nèi)聯(lián)到main()方法中了水评。
二、編譯優(yōu)化技術(shù)
1媚送、公共子表達式消除
公共子表達式消除是一個普遍應(yīng)用于各種編譯器的經(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)。
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)變量的取值范圍永遠在區(qū)間[0折联,foo.length)之內(nèi)粒褒,那在整個循環(huán)中就可以把數(shù)組的上下界檢查消除,這可以節(jié)省很多次的條件判斷操作诚镰。
3奕坟、方法內(nèi)聯(lián)
在前面的講解之中我們提到過方法內(nèi)聯(lián),它是編譯器最重要的優(yōu)化手段之一清笨,除了消除方法調(diào)用的成本之外月杉,它更重要的意義是為其他優(yōu)化手段建立良好的基礎(chǔ)
4、逃逸分析
逃逸分析(Escape Analysis)是目前Java虛擬機中比較前沿的優(yōu)化技術(shù)抠艾,它與類型繼承關(guān)系分析一樣苛萎,并不是直接優(yōu)化代碼的手段,而是為其他優(yōu)化手段提供依據(jù)的分析技術(shù)。
逃逸分析的基本行為就是分析對象動態(tài)作用域
:當一個對象在方法中被定義后腌歉,它可能被外部方法所引用蛙酪,例如作為調(diào)用參數(shù)傳遞到其他方法中,稱為方法逃逸翘盖。甚至還有可能被外部線程訪問到桂塞,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱為線程逃逸馍驯。
如果能證明一個對象不會逃逸到方法或線程之外阁危,也就是別的方法或線程無法通過任何途徑訪問到這個對象,則可能為這個變量進行一些高效的優(yōu)化泥彤,如下所示欲芹。
-
棧上分配(Stack Allocation)
:Java虛擬機中,在Java堆上分配創(chuàng)建對象的內(nèi)存空間幾乎是Java程序員都清楚的常識了吟吝,Java堆中的對象對于各個線程都是共享和可見的菱父,只要持有這個對象的引用,就可以訪問堆中存儲的對象數(shù)據(jù)剑逃。虛擬機的垃圾收集系統(tǒng)可以回收堆中不再使用的對象浙宜,但回收動作無論是篩選可回收對象,還是回收和整理內(nèi)存都需要耗費時間蛹磺。如果確定一個對象不會逃逸出方法之外粟瞬,那讓這個對象在棧上分配內(nèi)存將會是一個很不錯的主意,對象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀萤捆。 -
同步消除(Synchronization Elimination)
:線程同步本身是一個相對耗時的過程裙品,如果逃逸分析能夠確定一個變量不會逃逸出線程,無法被其他線程訪問俗或,那這個變量的讀寫肯定就不會有競爭市怎,對這個變量實施的同步措施也就可以消除掉。 -
標量替換(Scalar Replacement)
:標量(Scalar)是指一個數(shù)據(jù)已經(jīng)無法再分解成更小的數(shù)據(jù)來表示了辛慰,Java虛擬機中的原始數(shù)據(jù)類型(int区匠、long等數(shù)值類型以及reference類型等)都不能再進一步分解,它們就可以稱為標量帅腌。相對的驰弄,如果一個數(shù)據(jù)可以繼續(xù)分解,那它就稱作聚合量(Aggregate)速客,Java中的對象就是最典型的聚合量
戚篙。如果把一個Java對象拆散,根據(jù)程序訪問的情況挽封,將其使用到的成員變量恢復(fù)原始類型來訪問就叫做標量替換已球。如果逃逸分析證明一個對象不會被外部訪問臣镣,并且這個對象可以被拆散的話,那程序真正執(zhí)行的時候?qū)⒖赡懿粍?chuàng)建這個對象智亮,而改為直接創(chuàng)建它的若干個被這個方法使用到的成員變量來代替忆某。