《深入理解Java虛擬機(jī)-JVM高級特性與最佳實(shí)踐》學(xué)習(xí)總結(jié)(第十一章)

第十一章 晚期(運(yùn)行期)優(yōu)化
晚期(運(yùn)行期)優(yōu)化
目錄:

11.1 運(yùn)行期優(yōu)化什么?
11.2 解釋器與編譯器的分工
11.3 HotSpot虛擬機(jī)內(nèi)的即時(shí)編譯器
11.4 編譯優(yōu)化技術(shù)
11.5 Java與C/C++編譯器對比

11.1 運(yùn)行期優(yōu)化什么?

11.1.1最初代碼在運(yùn)行期所經(jīng)歷的事情:
在部分的商用虛擬機(jī)(Sun HotSpot、IBM J9)中,Java程序最初是通過解釋器(Interpreter)進(jìn)行解釋執(zhí)行的。
11.1.2現(xiàn)在代碼在運(yùn)行期所要經(jīng)歷的就是:
現(xiàn)在當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或者代碼快的運(yùn)行特別頻繁,就會(huì)把這些代碼認(rèn)定為“熱點(diǎn)代碼”(Hot Spot Code),為了提高熱點(diǎn)代碼的執(zhí)行效率,在運(yùn)行的時(shí)候,虛擬機(jī)就會(huì)把這些代碼編譯成與本地平臺相關(guān)的機(jī)器碼,并進(jìn)行各種層次的優(yōu)化,而完成這個(gè)任務(wù)的編譯器就叫做即使編譯器(Just In TIme Compiler,JIT)。

11.2 解釋器與編譯器的分工

解釋器與編譯器并存的架構(gòu)究竟是怎樣協(xié)同工作的?

  • 當(dāng)程序需要迅速啟動(dòng)和執(zhí)行的時(shí)候,解釋器可以首先發(fā)揮作用,省去編譯的時(shí)間,立即執(zhí)行。
  • 當(dāng)程序運(yùn)行后,隨著時(shí)間的推移,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲得更高的執(zhí)行效率。
  • 當(dāng)程序運(yùn)行環(huán)境中內(nèi)存資源限制較大,可以使用解釋執(zhí)行節(jié)約內(nèi)存,反之可以使用編譯執(zhí)行來提升效率。
  • 同時(shí),解釋器還可以作為編譯器激進(jìn)優(yōu)化時(shí)的一個(gè)"逃生門",讓編譯器根據(jù)概率選擇一些大多數(shù)時(shí)候都能提升運(yùn)行速度的優(yōu)化手段,當(dāng)激進(jìn)優(yōu)化的假設(shè)不成立,如加載了新類后類型繼承結(jié)構(gòu)出現(xiàn)變化、出現(xiàn)"罕見陷阱"(Uncommon Trap)時(shí)可以通過逆優(yōu)化(Deoptimization)退回到解釋執(zhí)行(部分沒有解釋器的虛擬機(jī)中也會(huì)采用不進(jìn)行激進(jìn)優(yōu)化的C1編譯器擔(dān)任"逃生門"的角色)

因此在整個(gè)虛擬機(jī)執(zhí)行架構(gòu)中,解釋器與編譯器經(jīng)常是相輔相成地配合工作的。

解釋器與編譯器的簡單交互
11.3 HotSpot虛擬機(jī)內(nèi)的即時(shí)編譯器

11.3.1 兩個(gè)內(nèi)置的即時(shí)編譯器

  • Client Compiler(C1)
  • Server Compiler(C2)

目前主流的HotSpot虛擬機(jī)中(Sun系列JDK1.6及之前版本的虛擬機(jī)),默認(rèn)是采用解釋器與其中一個(gè)編譯器直接配合的方式工作,程序使用哪個(gè)編譯器,取決于虛擬機(jī)運(yùn)行的模式,HotSpot虛擬機(jī)會(huì)根據(jù)自身版本與宿主機(jī)器的硬件性能自動(dòng)選擇運(yùn)行模式,用戶也可以使用-client 或 -server 參數(shù)去強(qiáng)制指定虛擬機(jī)運(yùn)行在Client模式還是Server模式

利用參數(shù)來指定虛擬機(jī)處于"解釋模式"、"編譯模式"還是"混合模式"。

混合模式:

java -version
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)

解釋模式:

java -Xint -version
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, interpreted mode)

編譯模式:

java -Xcomp -version
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, compiled mode)

那么當(dāng)虛擬機(jī)跑起來的時(shí)候,解釋與編譯又是怎樣互相合作的呢?目前最常見的就是分層編譯(Tiered Compilation)。
11.3.2 分層編譯(Tiered Compilation)
原因:由于即時(shí)編譯器編譯本地代碼需要占用程序運(yùn)行時(shí)間,要編譯出優(yōu)化程度更高的代碼,所花費(fèi)的時(shí)間可能越長;而且想要編譯出優(yōu)化程度更高的代碼,解釋器可能還要替編譯器收集性能監(jiān)控信息,這對解釋執(zhí)行的速度也有所影響,為了在程序啟動(dòng)響應(yīng)速度與效率之間達(dá)到最佳平衡,HotSpot虛擬機(jī)將會(huì)逐漸啟用分層編譯,該概念在JDK1.6時(shí)期出現(xiàn),在JDK1.7的Server模式虛擬中作為默認(rèn)編譯策略被開啟。
層次:

  • 第0層:程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控功能(Profiling),可觸發(fā)第1層編譯。
  • 第1層:也成為C1編譯,將字節(jié)碼編譯為本地代碼,進(jìn)行簡單可靠的優(yōu)化,如有必要加入性能監(jiān)控的邏輯。
  • 第2層(或者2層以上):也成為C2編譯,也是將字節(jié)碼編譯為本地代碼,但是會(huì)啟用一些編譯耗時(shí)較長的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化。

11.3.3 編譯對象和觸發(fā)條件
編譯對象:那些被當(dāng)作"熱點(diǎn)代碼"的,即:

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

對于第一種情況,由于是由方法調(diào)用觸發(fā)的編譯,那編譯器理所應(yīng)當(dāng)會(huì)以整個(gè)方法作為編譯對象,這種編譯也是虛擬機(jī)中標(biāo)準(zhǔn)的編譯方式。
其觸發(fā)編譯的整個(gè)過程如下圖:

方法調(diào)用計(jì)數(shù)器觸發(fā)即時(shí)編譯 .png

而對于后一中情況,盡管編譯動(dòng)作是由循環(huán)體所觸發(fā)的,但編譯器依然會(huì)以整個(gè)方法(而不是單獨(dú)的循環(huán)體)作為編譯對象。這種編譯方式因?yàn)榫幾g發(fā)生在方法執(zhí)行過程之中,因此被很形象地稱為棧上替換(On Stack Replacement, OSR)。
其觸發(fā)編譯的整個(gè)過程如下圖:

回邊調(diào)用計(jì)數(shù)器觸發(fā)即時(shí)編譯

那么,上面提到的"多次調(diào)用的方法"、"多次執(zhí)行的循環(huán)體",這個(gè)"多次"究竟是多少次?
為了回答這個(gè)問題,就要引出來我們的觸發(fā)即時(shí)編譯的條件了。
觸發(fā)條件:
我們要判斷一段代碼是不是熱點(diǎn)代碼,這個(gè)過程就叫做熱點(diǎn)探測。目前主要的熱點(diǎn)探測判定方式有兩種:

  • 基于采樣的熱點(diǎn)探測(Sample Based Hot Spot Detection):采用這種方法的虛擬機(jī)會(huì)周期性地檢查各個(gè)線程的棧頂,如果發(fā)現(xiàn)某個(gè)(或者某些)方法經(jīng)常出現(xiàn)在棧頂,那這個(gè)方法就是"熱點(diǎn)方法"?;诓蓸拥臒狳c(diǎn)探測的好處是簡單高效,還可以很容易第獲取方法調(diào)用關(guān)系(將調(diào)用堆棧展開即可),缺點(diǎn)是很難精確第確認(rèn)一個(gè)方法的熱度,容易因?yàn)槭艿骄€程阻塞或別的外界因素的影響而擾亂熱點(diǎn)探測。
  • 基于計(jì)數(shù)器的熱點(diǎn)探測(Counter Based Hot Spot Detection):采用這種方法的虛擬機(jī)會(huì)為每個(gè)方法(甚至是代碼塊)建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過一定的閥值就認(rèn)為它是"熱點(diǎn)方法"。這種統(tǒng)計(jì)方法實(shí)現(xiàn)起來麻煩一些,需要為每個(gè)方法建立并維護(hù)計(jì)數(shù)器,而且不能直接獲取到方法的調(diào)用關(guān)系。但是它的統(tǒng)計(jì)結(jié)果相對來說更加精確嚴(yán)謹(jǐn)。

在HotSpot虛擬機(jī)中使用的是第二種---基于計(jì)數(shù)器的熱點(diǎn)探測方法,因此它為每個(gè)方法準(zhǔn)備了兩個(gè)計(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ù)器超過閥值溢出了,就會(huì)觸發(fā)JIT編譯。

當(dāng)我們發(fā)出的編譯請求終于得到虛擬機(jī)的許可之后,我們就要開始做事情啦,那么要做哪些事情呢?
11.3.4 編譯過程要做的事情
在JIT編譯器進(jìn)行后臺編譯的過程中,編譯器做了哪些事情呢?
對于Client Compiler來說,它是一個(gè)簡單快速的三段式編譯器,主要的關(guān)注點(diǎn)在于局部性的優(yōu)化,而放棄了許多耗時(shí)較長的全局優(yōu)化手段。

  • 第一個(gè)階段,一個(gè)平臺獨(dú)立的前端將字節(jié)碼構(gòu)造成一種高級中間代碼表示(High-Level Intermediate Representation, HIR)。HIR使用靜態(tài)單分配(Static Single Assignment, SSA)的形式來代表代碼值,這可以使得一些在HIR的構(gòu)造之中和之后進(jìn)行的優(yōu)化動(dòng)作更容易實(shí)現(xiàn)。
  • 第二個(gè)階段,一個(gè)平臺相關(guān)的后端從HIR中產(chǎn)生低級中間代碼表示(Low-Level Intermediate Representation, LIR)。
  • 最后的階段是在平臺相關(guān)的后端使用線性掃描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窺孔(Peephole)優(yōu)化,然后產(chǎn)生機(jī)器代碼
    以下是一個(gè)圖形化的介紹,便于記憶:
Client Compiler架構(gòu)

那么Server Compiler又是怎樣的呢?
Server Compiler 則是專門面向服務(wù)端的典型應(yīng)用并為服務(wù)端的性能配置特別調(diào)整過的編譯器。它將會(huì)執(zhí)行所有的經(jīng)典的優(yōu)化動(dòng)作,如:無用代碼消除(Dead Code Elimination)等??偠灾?,它相對于Client Compiler編譯輸出的代碼質(zhì)量有所提高,可以減少本地代碼的執(zhí)行時(shí)間,從而抵消了額外的編譯時(shí)間開銷。

剛才一直在說編譯過程中要做的事情,那么在這個(gè)過程中,有沒有一些特別屌的技術(shù)來助我們編譯一臂之力呢?接下來就要聊到我們的編譯優(yōu)化技術(shù)這個(gè)東東啦!

11.4 編譯優(yōu)化技術(shù)

11.4.1以例子來說明幾個(gè)常用的優(yōu)化技術(shù)

static class B {
     int value;
     final int get() {
          return value;
    }
}

public void foo() {
      y = b.get();
      // do something...
      z = b.get();
      sum = y + z;
}

1.方法內(nèi)聯(lián)(Method Inlining)優(yōu)化之后:

public void foo() {
      y = b.value;
      // do something...
      z = b.value;
      sum = y + z;
}

2.冗余訪問消除(Redundant Loads Elimination)

public void foo() {
      y = b.value;
      // do something...
      z = y;
      sum = y + z;
}

3.復(fù)寫傳播(Copy Propagation)

public void foo() {
      y = b.value;
      // do something...
      y= y;
      sum = y + y;
}

4.無用代碼消除(Dead Code Elimination)

public void foo() {
      y = b.value;
      // do something...
      sum = y + y;
}

除了上邊用例子展示的那幾個(gè),肯定還有其他的優(yōu)化技術(shù)嘛,否則虛擬機(jī)豈不是也太讓人小瞧了嘛,來,我們接著看!
11.4.2 其他重要優(yōu)化技術(shù)

  • 公共子表達(dá)式消除
    含義:如果一個(gè)表達(dá)式E已經(jīng)被計(jì)算過了,并且從先前的計(jì)算到現(xiàn)在E中所有變量的值都沒有發(fā)生變化,那么E的這次出現(xiàn)就成為了公共子表達(dá)式。
    來人,把例子給我押上來!
int d = (c * b) * 12 + a + (a + b * c);

經(jīng)過我們的"嚴(yán)刑拷打"之后,該表達(dá)式就會(huì)變成:

int d = E * 13 + a * 2
  • 數(shù)組范圍檢查消除
    在編譯器根據(jù)數(shù)據(jù)流分析來確定array.length的值,并判斷當(dāng)前下標(biāo)是否在[0,array.length)之內(nèi)
  • 方法內(nèi)聯(lián)
    從表面上看,方法內(nèi)聯(lián)是把被調(diào)用方法的代碼"復(fù)制"到調(diào)用的方法之中,避免發(fā)生真實(shí)的方法調(diào)用。
    但是,內(nèi)部實(shí)現(xiàn)遠(yuǎn)不是這么簡單。由于存在非虛方法和虛方法的調(diào)用,故方法內(nèi)聯(lián)就會(huì)變得很復(fù)雜。對于非虛方法來說,可以直接內(nèi)心,不需要進(jìn)行對方法的選擇,而對于虛方法,則要進(jìn)行方法的選擇,因?yàn)橛卸鄳B(tài)這種特性存在。
    所以為了解決對虛方法的內(nèi)聯(lián),虛擬機(jī)引入了一種叫做"類型繼承關(guān)系分析"(Class Hierarchy Analysis, CHA)的技術(shù),這是一種基于整個(gè)應(yīng)用程序的類型分析技術(shù),它用于確定在目前已加載的類中,某個(gè)接口是否有多于一種的實(shí)現(xiàn),某個(gè)類是否存在子類且子類是否為抽象類等信息。
    利用CHA技術(shù)進(jìn)行方法內(nèi)聯(lián)的大致過程為:如果遇到虛方法,則會(huì)向CHA查詢次方法在當(dāng)前程序下是否由多個(gè)目標(biāo)版本可供選擇,
    如果只有一個(gè)版本,則就進(jìn)行內(nèi)聯(lián),不過這種內(nèi)聯(lián)屬于激進(jìn)優(yōu)化,需要預(yù)留一個(gè)"逃生門",稱為守護(hù)內(nèi)聯(lián)。如果程序的后續(xù)執(zhí)行過程中,虛擬機(jī)一直沒有加載到會(huì)令這個(gè)方法的接受者的繼承關(guān)系發(fā)生變化的類,那這個(gè)內(nèi)聯(lián)優(yōu)化的代碼就可以一直使用下去,但是如果加載了導(dǎo)致繼承關(guān)系發(fā)生變化的新類,那就需要拋棄掉已經(jīng)編譯的代碼,退回到解釋狀態(tài)執(zhí)行,或者重新進(jìn)行編譯。
    如果向CHA查詢出來的結(jié)果是有多個(gè)版本的目標(biāo)方法可供選擇,則編譯器還將會(huì)進(jìn)行最后一次努力,使用內(nèi)聯(lián)緩存(Inline Cache)來完成方法內(nèi)聯(lián),這是一個(gè)建立在目標(biāo)方法正常入口之前的緩存,它的工作原理大致是:在未發(fā)生方法調(diào)用之前,內(nèi)聯(lián)緩存狀態(tài)為空,當(dāng)?shù)谝淮握{(diào)用發(fā)生后,緩存記錄下方法接受者的版本信息,并且,每次進(jìn)行方法調(diào)用時(shí)都比較接受者版本,如果以后進(jìn)來的每次調(diào)用的方法接受者版本都是一樣的,那這個(gè)內(nèi)聯(lián)還可以一直用下去。如果發(fā)生了方法接受者不一致的情況,就說明程序真正使用到了虛方法的多態(tài)特性,這時(shí)候才會(huì)取消內(nèi)聯(lián),查找虛方法表進(jìn)行方法分派。
  • 逃逸分析
    其基本行為就是分析對象動(dòng)態(tài)作用域;當(dāng)一個(gè)對象在方法里被定義后,它可以被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他方法中,這種行為稱為方法逃逸。甚至還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實(shí)例變量,這種行為稱為線程逃逸。
    如果能證明一個(gè)對象不會(huì)逃逸到方法或線程之外,也就是別的方法或線程無法通過任何途徑訪問到這個(gè)對象,則可能為這個(gè)變量進(jìn)行一些高效的優(yōu)化,如:
  • 棧上分配(Stack Allocations)
  • 同步消除(Synchronization Elimination)
  • 標(biāo)量替換(Scalar Replacement)
11.5 Java與C/C++編譯器對比

簡單羅列幾點(diǎn)Java編譯器的劣勢:

  • 即時(shí)編譯器運(yùn)行占用的是用戶程序的運(yùn)行時(shí)間,具有很大的時(shí)間壓力,它能提供的優(yōu)化手段也嚴(yán)重受制于編譯成本。
  • 由于Java語言是動(dòng)態(tài)的類型安全語言,這就意味著虛擬機(jī)必須頻繁地進(jìn)行動(dòng)態(tài)檢查等各種具體行為,所以要消耗不少的運(yùn)行時(shí)間。
  • Java要對方法接受者進(jìn)行多態(tài)選擇的頻率要遠(yuǎn)遠(yuǎn)大于C/C++語言。
  • 由于Java語言是可以動(dòng)態(tài)擴(kuò)展的語言,所以很多全局的優(yōu)化都難以進(jìn)行,編譯器不得不時(shí)刻注意并隨著類型的變化而在運(yùn)行時(shí)撤銷或重新進(jìn)行一些優(yōu)化。
  • Java垃圾回收機(jī)制沒有C/C++效率高。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容