晚期(運行期)優(yōu)化
- 熱點代碼(Hot Spot Code):運行得特別頻繁的某個方法或代碼塊
- 被多次調(diào)用的方法敌蜂。
- 被多次執(zhí)行的循環(huán)體赤炒。
- 即時編譯器(Just In Time Compiler,簡稱JIT編譯器):為了提高熱點代碼的效率押桃,在運行時胎撤,把這些代碼編譯成與本地平臺相關(guān)的機器碼平酿,并進行各種層次的優(yōu)化的編譯器
HotSpot虛擬機內(nèi)的即時編譯器
解釋器與編譯器
當程序需要迅速啟動和執(zhí)行的時候,解釋器可以首先發(fā)揮作用受葛,省去編譯的時間题涨,立即執(zhí)行。在程序運行后总滩,隨著時間的推移纲堵,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后闰渔,可以獲取更高的執(zhí)行效率席函。
Client Compiler(C1編譯器)
Server Compiler(C2編譯器(也叫Opto編譯器))
HotSpot虛擬機會逐漸啟用分層編譯(Tiered Compilation)的策略
第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)化辐赞。
實施分層編譯后,Client Compiler和Server Compiler將會同時工作硝训,許多代碼都可能會被多次編譯响委,用Client Compiler獲取更高的編譯速度,用Server Compiler來獲取更好的編譯質(zhì)量窖梁,在解釋執(zhí)行的時候也無須再承擔收集性能監(jiān)控信息的任務(wù)赘风。
編譯對象與觸發(fā)條件
-
熱點代碼
- 被多次調(diào)用的方法。
- 編譯器理所當然地會以整個方法作為編譯對象纵刘,這種編譯也是虛擬機中標準的JIT編譯方式邀窃。
- 被多次執(zhí)行的循環(huán)體。
- 編譯器依然會以整個方法(而不是單獨的循環(huán)體)作為編譯對象假哎。 這種編譯方式因為編譯發(fā)生在方法執(zhí)行過程之中瞬捕,因此形象地稱之為棧上替換(On Stack Replacement鞍历,簡稱為OSR編譯,即方法棧幀還在棧上肪虎,方法就被替換了)
- 被多次調(diào)用的方法。
-
熱點探測
- 判斷一段代碼是不是熱點代碼劣砍,是不是需要觸發(fā)即時編譯的行為
- 基于采樣的熱點探測(Sample Based Hot Spot Detection)
- 虛擬機會周期性地檢查各個線程的棧頂,如果發(fā)現(xiàn)某個(或某些)方法經(jīng)常出現(xiàn)在棧頂扇救,那這個方法就是“熱點方法”秆剪。
- 基于計數(shù)器的熱點探測(Counter Based Hot Spot Detection)
- 虛擬機會為每個方法(甚至是代碼塊)建立計數(shù)器,統(tǒng)計方法的執(zhí)行次數(shù)爵政,如果執(zhí)行次數(shù)超過一定的閾值就認為它是“熱點方法”仅讽。
HotSpot虛擬機中使用的是第二種——基于計數(shù)器的熱點探測方法,它為每個方法準備了兩類計數(shù)器:方法調(diào)用計數(shù)器(Invocation Counter)和回邊計數(shù)器(Back Edge Counter)钾挟。
- 方法調(diào)用計數(shù)器
- 用于統(tǒng)計方法被調(diào)用的次數(shù)洁灵,它的默認閾值在Client模式下是1500次,在Server模式下是10 000次
-
當一個方法被調(diào)用時掺出,會先檢查該方法是否存在被JIT編譯過的版本徽千,如果存在,則優(yōu)先使用編譯后的本地代碼來執(zhí)行汤锨。 如果不存在已被編譯過的版本双抽,則將此方法的調(diào)用計數(shù)器值加1,然后判斷方法調(diào)用計數(shù)器與回邊計數(shù)器值之和是否超過方法調(diào)用計數(shù)器的閾值闲礼。 如果已超過閾值牍汹,那么將會向即時編譯器提交一個該方法的代碼編譯請求。
圖2 方法調(diào)用計數(shù)器觸發(fā)即時編譯 - 方法調(diào)用計數(shù)器熱度的衰減
- 當超過一定的時間限度柬泽,如果方法的調(diào)用次數(shù)仍然不足以讓它提交給即時編譯器編譯慎菲,那這個方法的調(diào)用計數(shù)器就會被減少一半,這段時間就稱為此方法統(tǒng)計的半衰周期(Counter Half Life Time)
- 回邊計數(shù)器
- 統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù)
- 回邊:在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令
- 建立回邊計數(shù)器統(tǒng)計的目的就是為了觸發(fā)OSR編譯
- 虛擬機運行在Client模式下锨并,回邊計數(shù)器閾值計算公式為:
- 方法調(diào)用計數(shù)器閾值(CompileThreshold)×OSR比率(OnStackReplacePercentage)/100露该。其中OnStackReplacePercentage默認值為933,如果都取默認值第煮,那Client模式虛擬機的回
邊計數(shù)器的閾值為13995解幼。
- 方法調(diào)用計數(shù)器閾值(CompileThreshold)×OSR比率(OnStackReplacePercentage)/100露该。其中OnStackReplacePercentage默認值為933,如果都取默認值第煮,那Client模式虛擬機的回
- 虛擬機運行在Server模式下,回邊計數(shù)器閾值的計算公式為:
- 方法調(diào)用計數(shù)器閾值(CompileThreshold)×(OSR比率(OnStackReplacePercentage)-解釋器監(jiān)控比率(InterpreterProfilePercentage)/100包警。其中OnStackReplacePercentage默認值為140撵摆,InterpreterProfilePercentage默認值為33,如果都取默認值揽趾,那Server模式虛擬機回邊計數(shù)器的閾值為10700台汇。
-
當解釋器遇到一條回邊指令時苛骨,會先查找將要執(zhí)行的代碼片段是否有已經(jīng)編譯好的版本篱瞎,如果有苟呐,它將會優(yōu)先執(zhí)行已編譯的代碼,否則就把回邊計數(shù)器的值加1俐筋,然后判斷方法調(diào)用計數(shù)器與回邊計數(shù)器值之和是否超過回邊計數(shù)器的閾值牵素。 當超過閾值的時候,將會提交一個OSR編譯請求澄者,并且把回邊計數(shù)器的值降低一些笆呆,以便繼續(xù)在解釋器中執(zhí)行循環(huán),等待
編譯器輸出編譯結(jié)果
圖3 回邊計數(shù)器觸發(fā)即時編譯 - 與方法計數(shù)器不同粱挡,回邊計數(shù)器沒有計數(shù)熱度衰減的過程赠幕,因此這個計數(shù)器統(tǒng)計的就是該方法循環(huán)執(zhí)行的絕對次數(shù)。 當計數(shù)器溢出的時候询筏,它還會把方法計數(shù)器的值也調(diào)整到溢出狀態(tài)榕堰,這樣下次再進入該方法的時候就會執(zhí)行標準編譯過程。
- 在確定虛擬機運行參數(shù)的前提下嫌套,這兩個計數(shù)器都有一個確定的閾值逆屡,當計數(shù)器超過閾值溢出了,就會觸發(fā)JIT編譯踱讨。
編譯過程
無論是方法調(diào)用產(chǎn)生的即時編譯請求魏蔗,還是OSR編譯請求,虛擬機在代碼編譯器還未完成之前痹筛,都仍然將按照解釋方式繼續(xù)執(zhí)行莺治,而編譯動作則在后臺的編譯線程中進行。
對于Client Compiler來說帚稠,它是一個簡單快速的三段式編譯器产雹,主要的關(guān)注點在于局部性的優(yōu)化,而放棄了許多耗時較長的全局優(yōu)化手段翁锡。
- 在第一個階段蔓挖,一個平臺獨立的前端將字節(jié)碼構(gòu)造成一種高級中間代碼表示(HighLevel Intermediate Representaion,HIR)。 HIR使用靜態(tài)單分配(Static Single Assignment,SSA)的形式來代表代碼值馆衔,這可以使得一些在HIR的構(gòu)造過程之中和之后進行的優(yōu)化動作更容易實現(xiàn)瘟判。 在此之前編譯器會在字節(jié)碼上完成一部分基礎(chǔ)優(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)生機器代碼左冬。
圖4 Client Compiler架構(gòu)
Server Compiler則是專門面向服務(wù)端的典型應(yīng)用并為服務(wù)端的性能配置特別調(diào)整過的編譯器桐筏,也是一個充分優(yōu)化過的高級編譯器,幾乎能達到GNU C++編譯器使用-O2參數(shù)時的優(yōu)化強度拇砰。另外梅忌,還可能根據(jù)解釋器或Client Compiler提供的性能監(jiān)控信息,進行一些不穩(wěn)定的激進優(yōu)化除破,如守護內(nèi)聯(lián)(Guarded Inlining)牧氮、 分支頻率預(yù)測(Branch Frequency Prediction)等。
- Server Compiler的寄存器分配器是一個全局圖著色分配器瑰枫,它可以充分利用某些處理器架構(gòu)(如RISC)上的大寄存器集合蹋笼。
編譯優(yōu)化技術(shù)
虛擬機設(shè)計團隊幾乎把對代碼的所有優(yōu)化措施都集中在了即時編譯器之中
公共子表達式消除
如果一個表達式E已經(jīng)計算過了,并且從先前的計算到現(xiàn)在E中所有變量的值都沒有發(fā)生變化躁垛,那么E的這次出現(xiàn)就成為了公共子表達式剖毯。 對于這種表達式,沒有必要花時間再對它進行計算教馆,只需要直接用前面計算過的表達式結(jié)果代替E就可以了逊谋。
數(shù)組邊界檢查消除
隱式異常處理
方法內(nèi)聯(lián)
消除方法調(diào)用的成本之外,并其他優(yōu)化手段建立良好的基礎(chǔ)
“類型繼承關(guān)系分析”(Class Hierarchy Analysis,CHA):
是一種基于整個應(yīng)用程序的類型分析技術(shù)土铺,它用于確定在目前已加載的類中胶滋,某個接口是否有多于一種的實現(xiàn),某個類是否存在子類悲敷、 子類是否為抽象類等信息究恤。
- 編譯器在進行內(nèi)聯(lián)時,如果是非虛方法,直接進行內(nèi)聯(lián)
- 如果遇到虛方法,則會向CHA查詢此方法在當前程序下是否有多個目標
版本可供選擇叛复。- 如果查詢結(jié)果只有一個版本胰挑,那也可以進行內(nèi)聯(lián)在抛,不過這種內(nèi)聯(lián)就屬于激進優(yōu)化,需要預(yù)留一個“逃生門”(Guard條件不成立時的Slow Path),稱為守護內(nèi)聯(lián)(Guarded Inlining)。 如果程序的后續(xù)執(zhí)行過程中雾叭,虛擬機一直沒有加載到會令這個方法的接收者的繼承關(guān)系發(fā)生變化的類,那這個內(nèi)聯(lián)優(yōu)化的代碼就可以一直使用下去落蝙。 但如果加載了導致繼承關(guān)系發(fā)生變化的新類织狐,那就需要拋棄已經(jīng)編譯的代碼暂幼,退回到解釋狀態(tài)執(zhí)行,或者重新進行編譯移迫。
- 如果向CHA查詢出來的結(jié)果是有多個版本的目標方法可供選擇旺嬉,則編譯器還將會進行最后一次努力,使用內(nèi)聯(lián)緩存(Inline Cache)來完成方法內(nèi)聯(lián)起意,這是一個建立在目標方法正常入口之前的緩存,它的工作原理大致是:在未發(fā)生方法調(diào)用之前病瞳,內(nèi)聯(lián)緩存狀態(tài)為空揽咕,當?shù)谝淮握{(diào)用發(fā)生后,緩存記錄下方法接收者的版本信息套菜,并且每次進行方法調(diào)用時都比較接收
者版本亲善,如果以后進來的每次調(diào)用的方法接收者版本都是一樣的,那這個內(nèi)聯(lián)還可以一直用下去逗柴。 如果發(fā)生了方法接收者不一致的情況蛹头,就說明程序真正使用了虛方法的多態(tài)特性,這時才會取消內(nèi)聯(lián)戏溺,查找虛方法表進行方法分派渣蜗。
逃逸分析
分析對象動態(tài)作用域:當一個對象在方法中被定義后,它可能被外部方法所引用旷祸,例如作為調(diào)用參數(shù)傳遞到其他方法中耕拷,稱為方法逃逸。 甚至還有可能被外部線程訪問到托享,譬如賦值給類變量或可以在其他線程中訪問的實例變量骚烧,稱為線程逃逸。
- 棧上分配(Stack Allocation):如果確定一個對象不會逃逸出方法之外闰围,那讓這個對象在棧上分配內(nèi)存將會是一個很不錯的主意赃绊。由于HotSpot虛擬機目前的實現(xiàn)方式導致棧上分配實現(xiàn)起來比較復雜,因此在HotSpot中暫時還沒有做這項優(yōu)化羡榴。
- 同步消除(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ù)程序訪問的情況,將其使用到的成員變量恢復原始類型來訪問就叫做標量替換汽绢。 如果逃逸分析證明一個對象不會被外部訪問吗跋,并且這個對象可以被拆散的話,那程序真正執(zhí)行的時候?qū)⒖赡懿粍?chuàng)建這個對象宁昭,而改為直接創(chuàng)建它的若干個被這個方法使用到的成員變量來代替跌宛。
Java與C/C++的編譯器對比
劣勢:
- 第一,因為即時編譯器運行占用的是用戶程序的運行時間积仗,具有很大的時間壓力疆拘,它能提供的優(yōu)化手段也嚴重受制于編譯成本。
- 第二寂曹,Java語言是動態(tài)的類型安全語言哎迄,這就意味著需要由虛擬機來確保程序不會違反語言語義或訪問非結(jié)構(gòu)化內(nèi)存。
- 第三隆圆,Java語言中雖然沒有virtual關(guān)鍵字漱挚,但是使用虛方法的頻率卻遠遠大于C/C++語言,這意味著運行時對方法接收者進行多態(tài)選擇的頻率要遠遠大于C/C++語言渺氧,也意味著即時編譯器在進行一些優(yōu)化(如前面提到的方法內(nèi)聯(lián))時的難度要遠大于C/C++的靜態(tài)優(yōu)化編譯器棱烂。
- 第四,Java語言是可以動態(tài)擴展的語言阶女,運行時加載新的類可能改變程序類型的繼承關(guān)系颊糜,這使得很多全局的優(yōu)化都難以進行。
- 第五秃踩,Java語言中對象的內(nèi)存分配都是堆上進行的衬鱼,只有方法中的局部變量才能在棧上分配。
優(yōu)勢 :
- 在C/C++中憔杨,別名分析(Alias Analysis)的難度就要遠高于Java鸟赫。
- Java編譯器另外一個紅利是由它的動態(tài)性所帶來的,由于C/C++編譯器所有優(yōu)化都在編譯期完成消别,以運行期性能監(jiān)控為基礎(chǔ)的優(yōu)化措施它都無法進行抛蚤,如調(diào)用頻率預(yù)測(Call Frequency Prediction)、 分支頻率預(yù)測(Branch Frequency Prediction)寻狂、 裁剪未被選擇的分支(Untaken Branch Pruning)等岁经,這些都會成為Java語言獨有的性能優(yōu)勢。