《深刻理解 Java 虛擬機》讀書筆記:晚期(運行期)優化

正文

在部分商用虛擬機(Sun HotSpot、IBM J9)中,Java 程序最初是經過解釋器進行解釋執行的,當虛擬機發現某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定爲「熱點代碼」。爲了提升熱點代碼的執行效率,在運行時,虛擬機會把這些代碼編譯成本地機器碼,並進行各類層次的優化。完成這個任務的編譯器稱爲即時編譯器(Just In Time Compiler,簡稱 JIT 編譯器)。前端

Java 虛擬機規範並無規定必需要有即時編譯器存在,更沒有限定或指導即時編譯器如何去實現。因此即時編譯器的功能徹底與虛擬機的具體實現相關。java

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

一、解釋器與編譯器

許多主流的商用虛擬機(如 HotSpot、J9),都採用解釋器與編譯器並存的架構。程序員

(1)解釋器、編譯器

當程序須要迅速啓動和執行時,解釋器能夠首先發揮做用,省去編譯的時間,當即執行。在程序運行後,隨着時間的推移,編譯器把愈來愈多的代碼編譯成本地代碼後,能夠獲取更高的執行效率。算法

當程序運行環境中內存資源限制較大(如部分嵌入式系統),可使用解釋執行節約內存,反之可使用編譯執行提高效率。後端

解釋器能夠做爲編譯器激進優化時的一個「逃生門」,讓編譯器根據機率選擇一些大多數時候都能提高運行速度的激進優化手段,當激進優化不成立時,能夠經過逆優化退回到解釋狀態繼續執行。數組

(2)C一、C2 編譯器

HopSpot 虛擬機內置了兩個即時編譯器,分別稱爲 Client Compiler(C1 編譯器)和 Server Compiler(C2 編譯器)。默認採用解釋器與其中一個編譯器配合的方式工做,程序使用哪一個編譯器,取決於虛擬機是以 Client 模式仍是 Server 模式運行。虛擬機會根據自身版本與宿主機器的硬件性能自動選擇運行模式,用戶也可使用「-client」或「-server」參數強制指定虛擬機的運行模式。緩存

(3)混合模式、解釋模式與編譯模式

  • 混合模式:解釋器與編譯器搭配使用的方式。
  • 解釋模式:所有代碼都使用解釋方式執行,編譯器徹底不介入工做。可以使用「-Xint」參數強制虛擬機運行於解釋模式。
  • 編譯模式:優先採用編譯方式執行,可是解釋器仍會在編譯沒法進行時介入執行過程。可以使用「-Xcomp」強制虛擬機運行於編譯模式。

(4)分層編譯

爲了在程序啓動響應速度與運行效率之間達到最佳平衡,HotSpot 虛擬機會逐漸啓用分層編譯的策略。架構

分層編譯根據編譯器編譯、優化的規模與耗時,劃分出不一樣的編譯層次,其中包括:性能

  • 第 0 層:程序解釋執行,解釋器不開啓性能監控功能,可觸發第 1 層編譯。
  • 第 1 層:也稱 C1 編譯,將字節碼編譯爲本地代碼,進行簡單、可靠的優化,必要時加入性能監控的邏輯。
  • 第 2 層(或 2 層以上):也稱 C2 編譯,也是將字節碼編譯爲本地代碼,但會啓用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。

實施分層編譯後,C一、C2 編譯器將會同時工做,用 C1 編譯器獲取更高的編譯速度,用 C2 編譯器獲取更好的編譯質量,解釋執行時也無須再承擔收集性能監控信息的任務。優化

二、編譯對象與觸發條件

(1)熱點代碼及編譯對象

熱點代碼:

  • 被屢次調用的方法。
  • 被屢次執行的循環體。

兩種熱點代碼的編譯對象都是整個方法。第一種熱點代碼的編譯,因爲是由方法調用觸發的,理所固然會以整個方法做爲編譯對象。第二種熱點代碼的編譯,儘管是由循環體觸發的,但編譯器仍會以整個方法(而不是單獨的循環體)做爲編譯對象。

棧上替換:
被屢次執行的循環體成爲熱點代碼時,所觸發的編譯。由於編譯發生在方法執行過程當中,所以稱之爲棧上替換(也稱 OSR 編譯),即方法棧幀還在棧上,方法就被替換了。

(2)熱點探測

判斷一段代碼是否是熱點代碼,是否是須要觸發即時編譯,這樣的行爲稱爲熱點探測。

熱點探測方式:

  • 基於採樣的熱點探測:虛擬機週期性地檢查各個線程的棧頂,若是發現某個方法常常出如今棧頂,那這個方法就是「熱點代碼」。實現簡單高效,但很難精確地確認方法的熱度。
  • 基於計數器的熱點探測:虛擬機爲每一個方法(甚至是代碼塊)創建計數器,統計方法的執行次數,若是執行次數超過必定閾值就認爲它是「熱點代碼」。實現麻煩,但統計結果更加精確嚴謹。

HotSpot 虛擬機使用的是基於計數器的熱點探測方法,它爲每一個方法準備了兩類計數器:

  • 方法調用計數器:統計方法被調用的次數。
  • 回邊計數器:統計一個方法中循環體代碼執行的次數。在字節碼中遇到控制流向後跳轉的指令稱爲「回邊」。

方法調用計數器觸發即時編譯:

回邊計數器觸發即時編譯:

三、編譯過程

默認設置下,不管是方法調用產生的即時編譯請求,仍是 OSR 編譯請求,虛擬機在代碼編譯還未完成以前,仍會按照解釋方式繼續執行,而編譯動做則在後臺的編譯線程中進行。

C1 編譯器編譯過程:

  • 第一階段:一個平臺獨立的前端將字節碼構形成一種高級中間代碼表示(HIR),HIR 使用靜態單分配(SSA)的形式來表明代碼值。在此以前,編譯器會在字節碼上進行方法內聯、常量傳播等優化。
  • 第二階段:一個平臺相關的後端從 HIR 中產生低級中間代碼表示(LIR)。在此以前,編譯器會在 HIR 上進行空值檢查消除、範圍檢查消除等優化。
  • 第三階段:平臺相關的後端使用線性掃描算法,在 LIR 上分配寄存器、作窺孔優化,而後產生機器代碼。

2、編譯優化技術

一、公共子表達式消除

若是一個表達式 E 已經計算過了,而且從先前計算到如今 E 中全部變量的值都沒有變化,那麼 E 的此次出現就成了公共子表達式。對於這種表達式,沒有必要再次進行計算,直接用前面計算過的表達式結果代替 E 便可。

二、數組邊界檢查消除

Java 語言訪問數組元素時,虛擬機系統會自動進行上下界的範圍檢查,一旦訪問超出範圍,將拋出一個運行時異常:java.lang.ArrayIndexOutOfBoundsException。

數組邊界檢查使得程序員即使沒有專門編寫防護代碼,也能夠避免大部分的溢出攻擊。但對於虛擬機的執行子系統來講,每次數組元素的讀寫都帶有一次隱含的條件斷定操做,若是程序中擁有大量數組訪問代碼,無疑大大增長了性能負擔。

編譯器能夠經過數據流分析斷定數組下標是否會越界,若是分析後肯定不會越界,那麼能夠把數組的上下界檢查消除。

三、方法內聯

把目標方法的代碼「複製」到發起調用的方法之中,避免發生真實的方法調用。

(1)類型繼承關係分析

對於一個虛方法,編譯期作內聯的時候根本沒法肯定應該使用哪一個方法版本,爲了解決這個問題,引入了類型繼承關係分析(Class Hierarchy Analysis,CHA)技術。CHA 用於肯定在目前已加載的類中,某個接口是否有多於一種的實現,某個類是否存在子類、子類是否爲抽象類等信息。

(2)方法內聯過程

  • 若是是非虛方法,直接進行內聯。
  • 若是是虛方法,則向 CHA 查詢是否有多個目標版本。
    • 若是隻有一個版本,則進行守護內聯。
    • 若是有多個版本,則使用內聯緩存完成方法內聯。

守護內聯:
當虛方法只有一個目標版本時,也能夠進行內聯,但這種內聯屬於激進優化,須要預留一個「逃生門」,這種內聯稱爲守護內聯。進行守護內聯時,若是後續執行過程當中,加載了致使繼承關係發生變化的新類,則須要拋棄已經編譯的代碼,退回到解釋狀態執行,或者從新進行編譯。

內聯緩存:
內聯緩存是一個創建在目標方法正常入口以前的緩存。它的工做原理是:在未發生方法調用前,內聯緩存狀態爲空,第一次調用發生後,緩存記錄下方法接收者的版本信息,而且每次進行方法調用時都會比較接收者版本。若是接收者版本一致,那麼這個內聯還能夠用下去,若是不一致,說明程序使用了虛方法的多態特性,此時會取消內聯,查找虛方法表進行方法分派。

四、逃逸分析

逃逸分析的基本行爲就是分析對象動態做用域:當一個對象在方法中定義後,若是它被外部方法所引用或被外部線程訪問到,那麼就說這個對象發生了逃逸。

  • 方法逃逸:對象被外部方法所引用,好比做爲調用參數傳遞到其餘方法中。
  • 線程逃逸:對象被外部線程訪問到。

若是能證實一個對象不會逃逸到方法或線程以外,也就是別的方法或線程沒法經過任何途徑訪問到這個對象,則可能爲這個變量進行一些高效的優化。

(1)棧上分配

若是肯定一個對象不會逃逸出方法以外,那麼可讓這個對象在棧上分配內存。這樣對象所佔用的內存空間就能夠隨棧幀出棧而銷燬,從而減小了垃圾收集系統的壓力。

(2)同步消除

若是肯定一個變量不會逃逸出線程,那麼這個變量的讀寫確定不會有競爭,所以能夠消除掉這個變量的線程同步措施。

(3)標量替換

若是肯定一個對象不會被外部訪問,而且這個對象能夠被拆散的話,那麼程序真正執行時可能不建立這個對象,而改成建立它的若干個被這個方法使用到的成員變量來代替,這個過程稱爲標量替換。

將對象拆分後,除了可讓對象的成員變量在棧上分配和讀寫以外,還能夠爲後續進一步的優化手段建立條件。

標量與聚合量:

  • 標量:若是一個數據沒法再分解成更小的數據來表示,則稱爲標量。好比 int、long 等原始數據類型。
  • 聚合量:若是一個數據能夠繼續分解,則稱爲聚合量。好比 Java 對象。
相關文章
相關標籤/搜索