溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JVM:晚期(運行期)優(yōu)化的深入理解

發(fā)布時間:2020-09-09 12:29:18 來源:腳本之家 閱讀:175 作者:邋遢的流浪劍客 欄目:編程語言

晚期(運行期)優(yōu)化

在部分的商用虛擬機中,Java程序最初是通過解釋器進行解釋執(zhí)行的,當虛擬機發(fā)現(xiàn)某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定為熱點代碼。為了提高熱點代碼的執(zhí)行效率,在運行時,虛擬機會將這些代碼編譯成與本地平臺相關的機器碼,并進行各種層次的優(yōu)化,完成這個任務的編譯器稱為即時編譯器

本章提到的編譯器、即時編譯器都是指HotSpot虛擬機內的即時編譯器,虛擬機也是特質HotSpot虛擬機

1、HotSpot虛擬機內的即時編譯器

1)、解釋器與編譯器

當程序需要迅速啟動和執(zhí)行的時候,解釋器可以首先發(fā)揮作用,省去編譯的時間,立即執(zhí)行。在程序運行后,隨著時間的推移,編譯器逐漸發(fā)揮作用,把越來越多的代碼編輯成本地代碼之后,可以獲取更高的執(zhí)行效率。當程序運行環(huán)境中內存資源限制較大,可以使用解釋執(zhí)行節(jié)約內存,反之可以使用編譯執(zhí)行來提升效率

JVM:晚期(運行期)優(yōu)化的深入理解

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

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

C:\Users\hxt>java -version
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode)
----------------------------------------------------------------------
C:\Users\hxt>java -Xint -version
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, interpreted mode)
----------------------------------------------------------------------
C:\Users\hxt>java -Xcomp -version
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, compiled mode)

為了在程序啟動響應速度與運行效率之間達到最佳平衡,HotSpot虛擬機會啟用分層編譯的策略,分層編譯根據編譯器編譯、優(yōu)化的規(guī)模與耗時,劃分出不同的編譯層次,其中包括:

  • 第0層,程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控功能,可觸發(fā)第1層編譯
  • 第1層,也稱為C1編譯,將字節(jié)碼編譯為本地代碼,進行簡單、可靠的優(yōu)化,如有必要將加入性能監(jiān)控的邏輯
  • 第2層,也稱為C2編譯,也是將字節(jié)碼編譯為本地代碼,但是會啟用一些編譯耗時較長的優(yōu)化,甚至會根據性能監(jiān)控信息進行一些不可靠的激進優(yōu)化

用C1編譯器獲取更高的編譯速度,用C2編譯器獲取更高的編譯質量,在解釋執(zhí)行的時候也無須再承擔收集性能監(jiān)控信息的任務

2)、編譯對象與觸發(fā)條件

在運行過程中會被即時編譯器編譯的熱點代碼有兩類:

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

對于第一種情況,由于是由方法調用觸發(fā)的編譯,因此編譯器會以整個方法作為編譯對象,這種編譯也是虛擬機標準的JIT編譯方式。而對于后一種情況,盡管編譯動作是由循環(huán)體所觸發(fā)的,但編譯器依然會以整個方法作為編譯對象。這種編譯方式因為編譯發(fā)生在方法執(zhí)行過程之中,因此稱之為棧上替換(OSR編譯,即方法幀還在棧上,方法就被替換了)

判斷一段代碼是不是熱點代碼,是不是需要觸發(fā)即時編譯,這樣的行為稱為熱點探測,其實進行熱點探測并不一定要知道方法具體被調用了多少次,目前主要的熱點探測判定方式有兩種:

  • 基于采用的熱點探測:采用這種方式的虛擬機會周期性地檢查各個線程的棧頂,如果發(fā)現(xiàn)某個方法經常出現(xiàn)在棧頂,那這個方法就是熱點方法。基于采樣的熱點探測的好處是實現(xiàn)簡單、高效,還可以很容易地獲取方法調用關系,缺點是很難精確地確認一個方法的熱度,容易因為受到線程阻塞或別的外界因素的影響而擾亂熱點探測
  • 基于計數(shù)器的熱點探測:采用這種方法的虛擬機會為每個方法建立計數(shù)器,統(tǒng)計方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過一定的閾值就認為它是熱點方法。這種統(tǒng)計方法實現(xiàn)起來麻煩一些,需要為每個方法建立并維護計數(shù)器,而且不能直接獲取到方法的調用關系,但是它的統(tǒng)計結果相對來說更加精確和嚴謹

在HotSpot虛擬機中使用的是基于計數(shù)器的熱點探測,因此它為每個方法準備了兩類計數(shù)器:方法調用計數(shù)器和回邊計數(shù)器

在確定虛擬機運行參數(shù)的前提下,這兩個計數(shù)器都有一個確定的閾值,當計數(shù)器超過閾值溢出了,就會觸發(fā)JIT編譯

方法調用計數(shù)器用于統(tǒng)計方法被調用的次數(shù),它的默認閾值在Client模式下是1500次,在Server模式下是10000 次,這個閾值可以通過虛擬機參數(shù)-XX:CompileThreshold來設定。當一個方法被調用時,會先檢查該方法是否存在被JIT編譯過的版本,如果存在,則優(yōu)先使用編譯后的本地代碼來執(zhí)行。如果不存在已被編譯過的版本,則將此方法的調用計數(shù)器值加1,然后判斷方法調用計數(shù)器與回邊計數(shù)器值之和是否查過方法調用計數(shù)器的閾值。如果已超過閾值,那么將會向即時編譯器提交一個該方法的代碼編譯請求

JVM:晚期(運行期)優(yōu)化的深入理解

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

回邊計數(shù)器,它的作用是統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼中遇到控制流向后跳轉的指令稱為 “回邊”。顯然,建立回邊計數(shù)器統(tǒng)計的目的就是為了觸發(fā)OSR編譯

關于回邊計數(shù)器的閾值,雖然HotSpot虛擬機也提供了一個類似于方法調用計數(shù)器閾值-XX:CompileThreshold的參數(shù)-XX:BackEdgeThreashold供用戶設置,但是當前的虛擬機實際上并未使用此參數(shù),因此我們需要設置另外一個參數(shù)-XX:OnStackReplacePercentage來簡介調整回邊計數(shù)器的閾值,其計算公式如下

  • 虛擬機運行在 Client 模式下,回邊計數(shù)器閾值計算公式為:

方法調用計數(shù)器閾值(CompileThreshold)× OSR 比率(OnStackReplacePercentage)/ 100

其中OnStackReplacePercentage默認值為933,如果都取默認值,那Client模式虛擬機的回邊計數(shù)器的閾值為 13995

  • 虛擬機運行在 Server 模式下,回邊計數(shù)器閾值的計算公式為:

方法調用計數(shù)器閾值(CompileThreshold)× (OSR 比率(OnStackReplacePercentage)- 解釋器監(jiān)控比率(InterpreterProfilePercentage)) / 100

​其中OnStackReplacePercentage默認值為140,InterpreterProfilePercentage默認值為33,如果都取默認值,那Server模式虛擬機回邊計數(shù)器的閾值為10700

當解釋器遇到一條回邊指令時,會先查找將要執(zhí)行的代碼片段是否有已經編譯好的版本,如果有,它將會有限執(zhí)行已編譯的代碼,否則就把回邊計數(shù)器的值加 1,然后判斷方法調用計數(shù)器與回邊計數(shù)器之和是否超過回邊計數(shù)器的閾值。當超過閾值的時候,將會提交一個OSR編譯請求,并且把回邊計數(shù)器的值降低一些,以便繼續(xù)在解釋器中執(zhí)行循環(huán),等待編譯器輸出編譯結果

JVM:晚期(運行期)優(yōu)化的深入理解

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

上述內容僅僅描述了Client VM的即時編譯方式,對于Server VM來說,執(zhí)行情況會比上面的描述更復雜一些

3)、編譯過程

在默認設置下,無論是方法調用產生的即時編譯請求,還是OSR編譯請求,虛擬機在代碼編譯器還未完成之前,都仍然將按照解釋方式繼續(xù)執(zhí)行,而編譯動作則在后臺的編譯線程中進行。用戶可以通過參數(shù)-XX: -BackgroundCompilation來禁止后臺編譯,在禁止后臺編譯后,一旦達到JIT的編譯條件,執(zhí)行線程向虛擬機提交編譯請求后將會一直等待,直到編譯過程完成后再開始執(zhí)行編譯器輸出的本地代碼

對于Client Compiler來說,它是一個簡單快速的三段式編譯器,主要的關注點在于局部性的優(yōu)化,而放棄了許多耗時較長的全局優(yōu)化手段

在第一個階段,一個平臺獨立的前端將字節(jié)碼構造成一種高級中間代碼表示(HIR)。HIR使用靜態(tài)單分配的形式代表代碼值,這可以使得一些在HIR的構造過程之中和之后進行的優(yōu)化動作更容易實現(xiàn)。在此之前編譯器會在字節(jié)碼上完成一部分基礎優(yōu)化,如方法內聯(lián)、常量傳播等優(yōu)化將會在字節(jié)碼被構造成 HIR之前完成。

在第二個階段,一個平臺相關的后端從HIR中產生低級中間代碼表示(LIR),而在此之前會在HIR上完成另外一些優(yōu)化,如空值檢查消除、范圍檢查消除等,以便讓HIR達到更高效的代碼表示形式。

最后階段是在平臺相關的后端使用線性掃描算法在LIR上分配寄存器,并在LIR上做窺孔優(yōu)化,然后產生機器代碼

Server Compiler是專門面向服務端的典型應用并為服務端的性能配置特別調整過的編譯器,也是一個充分優(yōu)化過的高級編譯器,還可以根據解釋器或Client Compiler提供的性能監(jiān)控信息,進行一些不穩(wěn)定的激進優(yōu)化

Server Compiler的寄存器分配器是一個全局圖著色分配器,它可以充分利用某些處理器架構上的大寄存器集合

2、編譯優(yōu)化技術

1)、公共子表達式消除

如果一個表達式E已經計算過了,并且從先前的計算到現(xiàn)在E中所有變量的值都沒有發(fā)生變化,那么E的這次出現(xiàn)就成為了公共子表達式。對于這種表達式,沒有必要花時間在對它進行計算,只需要直接用前面計算過的表達式結果代替E就可以了。如果這種優(yōu)化僅限于程序的基本塊內,便稱為局部公共子表達式消除,如果這種優(yōu)化的范圍涵蓋了多個基本塊,那就稱為全局公共子表達式消除

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

Java語言是一門動態(tài)安全的語言,數(shù)組邊界檢查是必須做的,但數(shù)組邊界檢查是不是必須在運行期間一次不漏地檢查則是可以商量的事情。比如,數(shù)組下標是一個常量,如foo[3],只要在編譯期根據數(shù)據流分析來確定foo.length的值,并判斷下標3沒有越界,執(zhí)行的時候就無須判斷了。數(shù)組訪問發(fā)生在循環(huán)之中,并且使用循環(huán)變量來進行數(shù)組訪問,如果編譯器只要通過數(shù)據流分析就可以判定循環(huán)變量的取值范圍永遠在區(qū)間[0,foo.length)之內,那在整個循環(huán)中就可以把數(shù)組的上下界檢查消除,這可以節(jié)省很多次的條件判斷操作

隱式異常處理:Java中空指針檢查和算數(shù)運算中除數(shù)為零的檢查都采用了隱式異常處理

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

在使用隱式異常優(yōu)化之后,虛擬機會把上面?zhèn)未a所表示的訪問過程變?yōu)槿缦聜未a

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

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

3)、方法內聯(lián)

方法內聯(lián)除了消除方法調用的成本之外,還為其他優(yōu)化手段建立良好的基礎

只有使用invokespecial指令調用的私有方法、實例構造器、父類方法以及使用invokestatic指令進行調用的靜態(tài)方法才是在編譯期進行解析的,除了上述4中方法之外,其他的Java方法調用都需要在運行時進行方法接收者的多態(tài)選擇,并且都有可能存在多于一個版本的方法接收者,Java語言中默認的實例方法是虛方法

對于一個虛方法,編譯器做內聯(lián)的時候根本無法確定應該使用哪個版本,為了解決虛方法的內聯(lián)問題,引入了一種名為類型繼承關系分析(CHA)的技術,這是一種基于整個應用程序的類型分析技術,它用于確定在目前已加載的類中,某個接口是否有多于一種的實現(xiàn),某個類是否存在子類、子類是否為抽象類等信息

編譯器在進行內聯(lián)時,如果是非虛方法,那么直接進行內聯(lián)就可以了,這時候的內聯(lián)是有穩(wěn)定前提保障的。如果遇到虛方法,則會向CHA查詢方法在當前程序下是否有多個目標版本可供選擇,如果查詢結果只有一個版本,那也可以進行內聯(lián),不過這種內聯(lián)就屬于激進優(yōu)化,需要預留一個逃生門稱為守護內聯(lián)。如果程序的后續(xù)執(zhí)行過程中,虛擬機一直沒有加載到會令這個方法的接收者的繼承關系發(fā)生變化的類,那這個內聯(lián)優(yōu)化的代碼就可以一直使用下去。但如果加載了導致繼承關系發(fā)生變化的新類,那就需要拋棄已經編譯的代碼,退回到解釋狀態(tài)執(zhí)行,或者重新進行編譯

如果向CHA查詢出來的結果是有多個版本的目標方法可供選擇,編譯器使用內聯(lián)緩存來完成方法內聯(lián),在未發(fā)生方法調用之前,內聯(lián)緩存狀態(tài)為空,當?shù)谝淮握{用發(fā)生后,緩存記錄下方法接收者的版本信息,并且每次進行方法調用時都比較接收者版本,如果以后進來的每次調用的方法接收者版本都是一樣的,那這個內聯(lián)還可以一直用下去。如果發(fā)生了方法接收者不一致的情況,就說明程序真正使用了虛方法的多態(tài)特性,這時才會取消內聯(lián),查找虛方法表進行方法分派

4)、逃逸分析

逃逸分析是為其他優(yōu)化手段提供依據的分析技術

逃逸分析的基本行為就是分析對象動態(tài)作用域:當一個對象在方法中被定義后,它可能被外部方法所引用,例如作為調用參數(shù)傳遞到其他方法中,稱為方法逃逸。甚至還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱為線程逃逸

如果能證明一個對象不會逃逸到方法或線程之外,也就是別的方法或線程無法通過任何途徑訪問到這個對象,則可能為這個變量進行一些高效的優(yōu)化:

  • 棧上分配:如果確定一個對象不會逃逸出方法之外,那讓這個對象在棧上分配內存將會是一個很不錯的主意,對象所占用的空間內存就可以隨棧幀出棧而銷毀。在一般應用中,不會逃逸的局部對象所占的比例很大,如果能使用棧上分配,那大量的對象就會隨著方法的結束而自動銷毀了,垃圾收集系統(tǒng)的壓力將會小很多
  • 同步消除:線程同步本身是一個相對耗時的過程,如果逃逸分析能夠確定一個變量不會逃逸出線程,無法被其他線程訪問,那這個變量的讀寫肯定就不會有競爭,對這個變量實施的同步措施也就可以消除掉
  • 標量替換:標量是指一個數(shù)據已經無法再分解成更小的數(shù)據來表示了,Java虛擬機中的原始數(shù)據類型(int、long等數(shù)值類型以及reference類型等)都不能再進一步分解,它們就可以稱為標量。相對的,如果一個數(shù)據可以繼續(xù)分解,那它就稱作聚合量,Java中的對象就是最典型的聚合量。如果把一個Java對象拆散,根據程序訪問的情況,將其使用到的成員變量恢復原始類型來訪問就叫做標量替換。如果逃逸分析證明一個對象不會被外部訪問,并且這個對象可以被拆散的話,那程序真正執(zhí)行的時候將可能不創(chuàng)建這個對象,而改為直接創(chuàng)建它的若干個被這個方法使用的成員變量來代替。將對象拆分后,除了可以讓對象的成員變量在棧上分配和讀寫外,還可以為后續(xù)進一步的優(yōu)化手段創(chuàng)建條件

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對億速云的支持。如果你想了解更多相關內容請查看下面相關鏈接

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI