java 优化 寄存器_JVM性能优化系列-(6) 晚期编译优化
6. 晚期編譯優(yōu)化
晚期編譯優(yōu)化主要是在運(yùn)行時(shí)做的一些優(yōu)化手段。
6.1 JIT編譯器
在部分的商用虛擬機(jī)中,java程序最初是通過(guò)解釋器(Interpreter) 進(jìn)行解釋執(zhí)行的,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊的運(yùn)行特別頻繁時(shí),就會(huì)把這些代碼認(rèn)定為“熱點(diǎn)代碼”(Hot Spot Code)。為了提高熱點(diǎn)代碼的執(zhí)行效率,在運(yùn)行時(shí),虛擬機(jī)將會(huì)把這些代碼編譯成與本地平臺(tái)相關(guān)的機(jī)器碼,并進(jìn)行各種層次的優(yōu)化,完成這個(gè)過(guò)程的編譯器稱為即時(shí)編譯器(Just In Time Compiler)
java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定即時(shí)編譯器應(yīng)該如何實(shí)現(xiàn),也沒(méi)有規(guī)定虛擬機(jī)必需擁有即時(shí)編譯器,這部分功能完全是虛擬機(jī)具體實(shí)現(xiàn)相關(guān)的內(nèi)容。本文中提及的編譯器、即時(shí)編譯器都是指HotSpot虛擬機(jī)內(nèi)的即時(shí)編譯器。
6.2 HotSpot虛擬機(jī)內(nèi)的即時(shí)編譯器
解釋器和編譯器
HotSpot虛擬機(jī)采用解釋器與編譯器并存的架構(gòu),解釋器與編譯器兩者各有優(yōu)勢(shì):
當(dāng)程序需要迅速啟動(dòng)和執(zhí)行的時(shí)候,解釋器可以首先發(fā)揮作用,省去編譯的時(shí)間,立即執(zhí)行
在程序運(yùn)行后,隨著時(shí)間的推移,編譯器逐漸發(fā)揮作用,把越來(lái)越多的代碼編譯成本地代碼之后,可以獲得更高的執(zhí)行效率
當(dāng)程序運(yùn)行環(huán)境中內(nèi)存資源限制較大,可以使用解釋執(zhí)行節(jié)約內(nèi)存,反之可以使用編譯執(zhí)行來(lái)提升效率
解釋器還可以作為編譯器激進(jìn)優(yōu)化的一個(gè)“逃生門(mén)”,讓編譯器根據(jù)概率選擇一些大多數(shù)時(shí)候都能提升運(yùn)行速度的優(yōu)化手段,當(dāng)激進(jìn)優(yōu)化的假設(shè)不成立時(shí),可以通過(guò)逆優(yōu)化退回到解釋狀態(tài)繼續(xù)執(zhí)行。
HotSpot虛擬機(jī)中內(nèi)置了兩個(gè)即時(shí)編譯器,分別稱為Client Compiler和Server Compiler,或者簡(jiǎn)稱為C1編譯器和C2編譯器,虛擬機(jī)默認(rèn)采用解釋器與其中一個(gè)編譯器直接配合的方式工作
由于即時(shí)編譯器編譯本地代碼需要占用程序運(yùn)行時(shí)間,要編譯出優(yōu)化程度更高的代碼,所花費(fèi)的時(shí)間可能更長(zhǎng);而且想要編譯出優(yōu)化程度更高的代碼,解釋器可能還要替編譯器收集性能監(jiān)控信息,這對(duì)解釋執(zhí)行的速度也有影響。HotSpot虛擬機(jī)采用分層編譯(Tiered Compilation)的策略,其中包括:
第0層:程序解釋執(zhí)行,解釋器不開(kāi)啟性能監(jiān)控功能(Profiling),可觸發(fā)第1層編譯
第1層:也稱為C1編譯,將字節(jié)碼編譯為本地代碼,進(jìn)行簡(jiǎn)單、可靠的優(yōu)化,如有必要將加入性能監(jiān)控的邏輯
第2層:也稱為C2編譯,也是將字節(jié)碼編譯為本地代碼,但是會(huì)啟用一些編譯耗時(shí)較長(zhǎng)的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化
編譯對(duì)象與觸發(fā)條件
在運(yùn)行過(guò)程中會(huì)被即時(shí)編譯器編譯的“熱點(diǎn)代碼”有兩類:
被多次調(diào)用的方法
被多次執(zhí)行的循環(huán)體
在這兩種情況下,都是以整個(gè)方法作為編譯對(duì)象,這種編譯方式被稱為棧上替換。
判斷一段代碼是不是熱點(diǎn)代碼,是不是需要觸發(fā)即時(shí)編譯,這樣的行為稱為熱點(diǎn)探測(cè)(Hot Spot Detection),目前主要的熱點(diǎn)探測(cè)判定方式有兩種:
基于采樣的熱點(diǎn)探測(cè)(Sample Based Hot Spot Detection):采用這種方法的虛擬機(jī)會(huì)周期性地檢查各個(gè)線程地棧頂,如果發(fā)現(xiàn)某個(gè)方法經(jīng)常出現(xiàn)在棧頂,那這個(gè)方法就是“熱點(diǎn)方法”
基于計(jì)數(shù)器的熱點(diǎn)探測(cè)(Counter Based Hot Spot Detection):采用這個(gè)種方法的虛擬機(jī)會(huì)為每個(gè)方法建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過(guò)一定的閾值就認(rèn)為它是“熱點(diǎn)方法”
在HotSpot虛擬機(jī)中使用的是第二種,因此它為每個(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ù)器超過(guò)閾值溢出了,就會(huì)觸發(fā)JIT編譯。
方法調(diào)用計(jì)數(shù)器:用于統(tǒng)計(jì)方法被調(diào)用的次數(shù),它的默認(rèn)閾值在Client模式下是1500次,在Server模式在是10000次,可通過(guò)-XX: CompileThreshold來(lái)設(shè)定。
如果不做任何設(shè)置,方法調(diào)用計(jì)數(shù)器統(tǒng)計(jì)的不是方法被調(diào)用的絕對(duì)次數(shù),而是一個(gè)相對(duì)執(zhí)行頻率,即一段時(shí)間之內(nèi)方法被調(diào)用的次數(shù)。當(dāng)超過(guò)一定的時(shí)間限度,如果方法的調(diào)用次數(shù)仍然不足以讓它提交給即時(shí)編譯器編譯,那這個(gè)方法的調(diào)用計(jì)數(shù)器就會(huì)被減少一半,這個(gè)過(guò)程稱為方法調(diào)用計(jì)數(shù)器的熱度衰減(Counter Decay)
回邊計(jì)數(shù)器:用于統(tǒng)計(jì)一個(gè)方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為“回邊”(Back Edge)。
與方法計(jì)數(shù)器不同,回邊計(jì)數(shù)器沒(méi)有計(jì)數(shù)熱度衰減的過(guò)程,因此這個(gè)計(jì)數(shù)器統(tǒng)計(jì)的就是該方法循環(huán)執(zhí)行的絕對(duì)次數(shù)。
6.3 編譯過(guò)程
Server Compiler和Client Compiler兩個(gè)編譯器的編譯過(guò)程是不一樣的。
Client Compiler是一個(gè)簡(jiǎn)單快速的三段式編譯器,主要的關(guān)注點(diǎn)在于局部性的優(yōu)化,而放棄了許多耗時(shí)較長(zhǎng)的全局優(yōu)化手段。
第一個(gè)階段:使用一個(gè)平臺(tái)獨(dú)立的前端將字節(jié)碼構(gòu)造成一種高級(jí)中間代碼表示(High-Level Intermediate Representaion, HIR)。HIR使用靜態(tài)單分配(Static Single Assignment, SSA)的形式來(lái)代表代碼值,這可以使得一些在HIR的構(gòu)造過(guò)程之中和之后進(jìn)行的優(yōu)化動(dòng)作更容易實(shí)現(xiàn)。在此之前編譯器會(huì)在字節(jié)碼上完成一部分基礎(chǔ)優(yōu)化,如方法內(nèi)聯(lián)、常量傳播等
第二個(gè)階段:使用一個(gè)平臺(tái)相關(guān)的前端從HIR中產(chǎn)生低級(jí)中間代碼表示(Low-Level Intermediate Representaion, LIR),而在此之前會(huì)在HIR上完成另外一些優(yōu)化,如空值檢查清除、范圍檢查清除等
最后階段:使用平臺(tái)相關(guān)的后端使用線性掃描算法(Linear Scan Register Allocation)在LIR上分離寄存器,并在LIR上做窺孔(Peephole)優(yōu)化,然后產(chǎn)生機(jī)器代碼
Server Compiler是專門(mén)面向服務(wù)端的典型應(yīng)用并為服務(wù)端的性能配置特別調(diào)整過(guò)的編譯器,它會(huì)執(zhí)行所有經(jīng)典的優(yōu)化動(dòng)作。Server Compiler的寄存器分配器是一個(gè)全局圖著色分配器,它可以充分利用某些處理器架構(gòu)上的大寄存器集合。以即時(shí)編譯的標(biāo)準(zhǔn)來(lái)看,Server Compiler編譯速度比較緩慢,但依然遠(yuǎn)遠(yuǎn)超過(guò)傳統(tǒng)的靜態(tài)優(yōu)化編譯器,而且相對(duì)于Client Compiler編譯輸出的代碼質(zhì)量有所提高,可以減少本地代碼的執(zhí)行時(shí)間,從而抵消了額外的編譯時(shí)間開(kāi)銷.
6.4 編譯優(yōu)化技術(shù)
在即時(shí)編譯器中采用的優(yōu)化技術(shù)有很多,本節(jié)主要針對(duì)以下四種優(yōu)化技術(shù):
語(yǔ)言無(wú)關(guān)的經(jīng)典優(yōu)化技術(shù)之一:公共子表達(dá)式消除
語(yǔ)言相關(guān)的經(jīng)典優(yōu)化技術(shù)之一:數(shù)組范圍檢查消除
最重要的優(yōu)化技術(shù)之一:方法內(nèi)聯(lián)
最前沿的優(yōu)化技術(shù)之一:逃逸分析
公共子表達(dá)式消除
公共子表達(dá)式消除是一個(gè)普遍應(yīng)用與各種編譯器的經(jīng)典優(yōu)化技術(shù),它的含義是:
如果一個(gè)表達(dá)式E已經(jīng)計(jì)算過(guò)了,并且從先前的計(jì)算到現(xiàn)在E中的所有變量的值都沒(méi)有發(fā)生變化,那么E的這次出現(xiàn)就成為了公共子表達(dá)式
對(duì)于這種表達(dá)式,沒(méi)有必要花時(shí)間再對(duì)它進(jìn)行計(jì)算,只需要直接用前面計(jì)算過(guò)的表達(dá)式結(jié)果替代E就可以了
如果這種優(yōu)化僅限于程序的基本塊內(nèi),便稱為局部公共子表達(dá)式消除(Local Common Subexpression Elimination),如果這種優(yōu)化的范圍涵蓋了多個(gè)基本塊,那就稱為全局公共子表達(dá)式消除(Global Common Subexpression Elimination)。
數(shù)組邊界檢查消除
組邊界檢查消除(Array Bounds Checking Elimination)是即時(shí)編譯器中的一項(xiàng)語(yǔ)言相關(guān)的經(jīng)典優(yōu)化技術(shù)。由于java語(yǔ)言中訪問(wèn)數(shù)組元素時(shí),系統(tǒng)將會(huì)自動(dòng)進(jìn)行上下界的范圍檢查,這必定會(huì)造成性能負(fù)擔(dān)。為了安全,數(shù)組邊界檢查是必須做的,但數(shù)組邊界檢查是否必須一次不漏的執(zhí)行則是可以“商量”的事情。例如編譯器通過(guò)數(shù)據(jù)流分析判定數(shù)組下標(biāo)的取值永遠(yuǎn)在[0,數(shù)組.length)之內(nèi),就可以把數(shù)組的上下界檢查消除
從更高的角度看,大量安全檢查使編寫(xiě)java程序更簡(jiǎn)單,但也造成了更多的隱式開(kāi)銷,對(duì)于這些隱式開(kāi)銷,除了盡可能把運(yùn)行期檢查提到編譯期完成的思路之外,還可以使用隱式異常處理:
if(x != null){
return x.value;
}else{
throw new NullPointException();
}
隱式異常優(yōu)化后:
try{
return x.value;
}catch(segment_fault){
uncommon_trap();
}
虛擬機(jī)會(huì)注冊(cè)一個(gè)Segment Fault信號(hào)的異常處理器(uncommon_trap()),這樣x不為空時(shí),不會(huì)額外消耗一次對(duì)foo判空的開(kāi)銷。代價(jià)是當(dāng)x為空時(shí),必須轉(zhuǎn)入異常處理器中恢復(fù)并拋出NullPointException,速度遠(yuǎn)比一次判空檢查慢。
方法內(nèi)聯(lián)
方法內(nèi)聯(lián)是編譯器最重要的優(yōu)化手段之一,除了消除方法調(diào)用成本之外,更重要的意義是為其他優(yōu)化手段建立良好的基礎(chǔ)。方法內(nèi)聯(lián)的優(yōu)化行為只不過(guò)是把目標(biāo)方法的代碼“復(fù)制”到發(fā)起調(diào)用的方法之中,避免發(fā)生真實(shí)的方法調(diào)用而已。但實(shí)際上java虛擬機(jī)中的內(nèi)聯(lián)過(guò)程遠(yuǎn)遠(yuǎn)沒(méi)有那么簡(jiǎn)單,因?yàn)閖ava中的方法大多數(shù)是虛方法,虛方法在編譯期做內(nèi)聯(lián)的時(shí)候根本無(wú)法確定應(yīng)該使用哪個(gè)方法版本
對(duì)此java虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)想了很多辦法,首先是引入了一種名為“類型繼承關(guān)系分析”(Class Hierarchy Analysis, CHA)的技術(shù),這是一種基于整個(gè)應(yīng)用程序的類型分析技術(shù),它用于確定在目前已加載的類中,某個(gè)接口是否有多余一種的實(shí)現(xiàn),某個(gè)類是否存在子類、子類是否為抽象類等信息。
編譯器在進(jìn)行內(nèi)聯(lián):
非虛方法:直接進(jìn)行內(nèi)聯(lián),這時(shí)候的內(nèi)聯(lián)是有穩(wěn)定前提保障的
虛方法:向CHA查詢此方法只在當(dāng)前程序下是否有多個(gè)目標(biāo)版本可供選擇
只有一個(gè):可以進(jìn)行內(nèi)聯(lián),不過(guò)這種內(nèi)聯(lián)屬于激進(jìn)優(yōu)化,需要預(yù)留一個(gè)“逃生門(mén)”,稱為守護(hù)內(nèi)聯(lián)(Guarded Inlining)。如果程序的后續(xù)執(zhí)行過(guò)程中,虛擬機(jī)一直沒(méi)有加載到會(huì)令這個(gè)方法的接收者的繼承關(guān)系發(fā)生變化的類,那這個(gè)內(nèi)聯(lián)游湖的代碼就可以一直使用下去。否則,就需要拋棄已經(jīng)編譯的代碼,退回到解釋狀態(tài)執(zhí)行,或者重新進(jìn)行編譯
有多個(gè)版本:編譯器還將進(jìn)行最后一次努力,使用內(nèi)聯(lián)緩存(Inline Cache)來(lái)完成方法內(nèi)聯(lián)。工作原理大致是:在未發(fā)生方法調(diào)用之前,內(nèi)聯(lián)緩存狀態(tài)為空,當(dāng)?shù)谝淮握{(diào)用發(fā)生后,緩存記錄下方法接收者的版本信息,并且每次進(jìn)行方法調(diào)用時(shí)都比較接收者版本,如果以后進(jìn)來(lái)的每次調(diào)用的方法接收者版本都是一樣的,那這個(gè)內(nèi)聯(lián)還可以一直用下去。如果發(fā)生了方法接收者不一致的情況,就說(shuō)明程序真正使用了虛方法的多態(tài)特性,這時(shí)才會(huì)取消內(nèi)聯(lián),查找虛方法表進(jìn)行方法分派
逃逸分析
逃逸分析(Escape Analysis)是目前java虛擬機(jī)中比較前沿的優(yōu)化技術(shù),它與類型繼承關(guān)系分析一樣,并不是直接優(yōu)化代碼的手段,而是為其他優(yōu)化手段提供依據(jù)的分析技術(shù)。其基本行為是分析對(duì)象動(dòng)態(tài)作用域:當(dāng)一個(gè)對(duì)象在方法中被定義后,它可能被外部方法所引用,如作為調(diào)用參數(shù)傳遞到其他方法中,稱為方法逃逸;被外部線程訪問(wèn)到,稱為線程逃逸。
如果能證明一個(gè)對(duì)象不會(huì)逃逸到方法或線程之外,則可能為這個(gè)變量進(jìn)行一些高效的優(yōu)化:
棧上分配(Stack Allocation):將對(duì)象在棧上分配內(nèi)存,這樣就可以使對(duì)象所占內(nèi)存空間隨棧幀出棧而銷毀,減小垃圾收集系統(tǒng)的壓力
同步消除(Synchronization Elimination):對(duì)象無(wú)法被其他線程訪問(wèn),這個(gè)變量的讀寫(xiě)肯定不會(huì)有競(jìng)爭(zhēng),對(duì)這個(gè)變量實(shí)施的同步措施也就可以消除掉
標(biāo)量替換(Scalar Replacement):標(biāo)量(Scalar)是指一個(gè)數(shù)據(jù)已經(jīng)無(wú)法再分解成更小的數(shù)據(jù)來(lái)表示。如果逃逸分析證明一個(gè)對(duì)象不會(huì)被外部訪問(wèn),并且這個(gè)對(duì)象可以被拆散的話,那程序真正執(zhí)行的時(shí)候可能不創(chuàng)建這個(gè)對(duì)象,而直接創(chuàng)建它的成員變量來(lái)代替。將對(duì)象拆分后,除了可以讓對(duì)象的成員變量在棧上分配和讀寫(xiě)之外,還可以為后續(xù)進(jìn)一步的優(yōu)化手段創(chuàng)建條件。
6.5 java與C/C++的編譯器對(duì)比
java與C/C++的編譯器對(duì)比實(shí)際上代表了最經(jīng)典的即時(shí)編譯器與靜態(tài)編譯器的對(duì)比。java虛擬機(jī)的即時(shí)編譯器與C/C++的靜態(tài)優(yōu)化編譯器相比,可能會(huì)由于下列原因而導(dǎo)致輸出的本地代碼有一些劣勢(shì):
即時(shí)編譯器運(yùn)行時(shí)占用的是用戶程序的運(yùn)行時(shí)間,因此即時(shí)編譯器不敢隨便引入大規(guī)模的優(yōu)化技術(shù),而編譯的時(shí)間成本在靜態(tài)優(yōu)化編譯器中并不是主要的關(guān)注點(diǎn)
java語(yǔ)言是動(dòng)態(tài)的類型安全語(yǔ)言,這就意味著虛擬機(jī)必須頻繁地進(jìn)行安全檢查
java語(yǔ)言中虛方法的使用頻率遠(yuǎn)遠(yuǎn)大于C/C++語(yǔ)言,導(dǎo)致即時(shí)編譯器在進(jìn)行一些優(yōu)化時(shí)的難度要遠(yuǎn)大于C/C++的靜態(tài)優(yōu)化編譯器
java語(yǔ)言時(shí)可以動(dòng)態(tài)擴(kuò)展的語(yǔ)言,運(yùn)行時(shí)加載新的類可能改變程序類型的繼承關(guān)系,導(dǎo)致許多全局的優(yōu)化措施都只能以激進(jìn)優(yōu)化的方式來(lái)完成
java虛擬機(jī)中對(duì)象的內(nèi)存分配都是在堆上進(jìn)行的,而C/C++的對(duì)象則有多種分配方式,而且C/C++中主要由用戶程序代碼來(lái)回收分配的內(nèi)存,因此運(yùn)行效率上比垃圾收集機(jī)制要高
上面說(shuō)的java語(yǔ)言相對(duì)C/C++的劣勢(shì)都是為了換取開(kāi)發(fā)效率上的優(yōu)勢(shì)而付出的代價(jià),而且還有許多優(yōu)化是java的即時(shí)編譯器能做而C/C++的靜態(tài)優(yōu)化編譯器不能做或者不好做的,如別名分析、調(diào)用頻率預(yù)測(cè)、分支頻率預(yù)測(cè)、裁剪為被選擇的分支等.
本文由『后端精進(jìn)之路』原創(chuàng),首發(fā)于博客 http://teckee.github.io/ , 轉(zhuǎn)載請(qǐng)注明出處
搜索『后端精進(jìn)之路』關(guān)注公眾號(hào),立刻獲取最新文章和價(jià)值2000元的BATJ精品面試課程。
總結(jié)
以上是生活随笔為你收集整理的java 优化 寄存器_JVM性能优化系列-(6) 晚期编译优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: QQ厘米秀在哪 厘米秀怎么玩
- 下一篇: java给qq发消息_QQ发送消息