要點提煉| 理解JVM之程序編譯&代碼優化

本篇將介紹程序編譯時期的代碼優化手段,分成兩個階段:

  • 概述
  • 早期(編譯期)優化
  • 晚期(運行期)優化

1.概述

a.由於對Java語言的編譯期理解不同,可以分出幾個時期:

  • 前端編譯器
    • 作用:把Java代碼轉變成字節碼
    • 代表:Sun的Javac、Eclipse JDT中的增量式編譯器(ECJ)
    • 該時期的優化主要用於提升程序的編碼效率
  • 後端運行期編譯器/JIT編譯器
    • 作用:把字節碼轉變成本地機器碼
    • 代表:HotSpot VM的C1、C2編譯器
    • 該時期的優化主要用於提升程序的運行效率
  • 靜態提前編譯器/AOT編譯器
    • 作用:直接把Java代碼編譯成本地機器碼
    • 代表:GNU Compiler for the Java(GCJ)、Excelsior JET

b.Java即時編譯器與C/C++靜態編譯器的對比

  • 即時編譯器運行需要佔用程序運行時間,使得優化手段受制於編譯成本,否則用戶將在啓動程序察覺到重大延遲;而靜態編譯器的編譯時間成本不是重點
  • 靜態編譯器所有優化都在編譯期完成,而即時編譯器的動態性是把雙刃劍,一方面要求虛擬機頻繁進行動態檢查從而消耗大量運行時間,而且難以全局優化、只能以激進優化來完成,另一方面擁有運行期性能監控的優化措施,如調用頻率預測、分支頻率預測、裁剪未被選擇的分支等
  • Java中使用虛方法的頻率遠大於C/C++,表示運行時對方法接收者進行多態選擇的頻率更大,因此在進行某些優化難度會更大
  • Java在堆上進行對象的內存分配,而C/C++可在堆、棧上分配,減輕了內存回收的壓力;且C/C++中主要由用戶程序代碼回收內存,不存在無用對象的篩選,相比於垃圾收集機制運行效率更高

2.早期(編譯期)優化

幾乎所有語言都提供一些語法糖來方便開發,或能提高效率、或能提升語法的嚴謹性、或能減少編碼出錯的機會,下面是幾種常見語法糖:

  • 泛型與類型擦除
    • C#的泛型是真實泛型:無論在程序源碼、編譯後的IL、還是運行期的CLR中都是切實存在的,List<int>和List<String>在系統運行期生成,有自己的虛方法表和類型數據,屬於不同的類型,這種實現稱爲類型膨脹
    • Java的泛型是僞泛型:只在程序源碼中存在,在編譯後的字節碼文件中就已替換爲原生類型,並在相應的地方插入了強制轉型代碼,因此ArrayList<int>與ArrayList<String>是同一個類,這種實現稱爲類型擦除
  • 自動裝箱、拆箱
  • 遍歷循環
  • 條件編譯:使用條件爲常量的if語句

更多Java語法糖系列


3.晚期(運行期)優化

a.HotSpot虛擬機採用解釋器與編譯器並存的架構,交互情況:

  • 當程序需要迅速啓動和執行時,解釋器可以先發揮作用,從而省去編譯時間
  • 程序運行後,隨着時間的推移,編譯器逐漸發揮作用,把更多代碼編譯成本地代碼,從而獲取更高的執行效率
  • 如果程序運行環境受內存資源限制較大,可以用解釋執行節約內存,反之可以用編譯執行提升效率
  • 解釋器可作爲編譯器激進優化的逃生門,當激進優化不成立時,如加載新類後類型繼承結構出現變化、出現罕見陷阱,可通過逆優化退回到解釋狀態繼續執行。如圖:

解釋器與編譯器的交互

有上圖可見,HotSpot虛擬機中內置了兩個即時編譯器:Client Compiler(C1編譯器和)和Server Compiler(C2編譯器),搭配模式:

  • 混合模式(Mixed Mode):默認採用解釋器與其中一個編譯器進行配合工作,虛擬機會根據自身版本與宿主機器的硬件性能自動選擇運行模式和編譯器,用戶可以使用-client-server參數去強制指定虛擬機運行在Client模式或Server模式。
  • 解釋模式(Interpreted Mode):使用參數-Xint,編譯器不工作,都使用解釋方式執行。
  • 編譯模式(Compiled Mode):使用參數-Xcomp,優先採用編譯方式執行,但解釋器仍然要在編譯無法進行的情況下介入執行過程。

b.HotSpot即時編譯器的編譯對象:熱點代碼

  • 分類:
    • 被多次調用的方法:採用JIT編譯方式,以整個方法作爲編譯對象
    • 被多次執行的循環體:採用OSR編譯方式,發生在方法執行過程中,仍以整個方法作爲編譯對象
  • 判斷方式:通過熱點探測
    • 基於採樣的熱點探測(Sample Based Hot Spot Detection):週期性檢查各個線程的棧頂,常出現在棧頂的方法就是熱點方法
      • 好處:實現簡單、高效、易於獲取方法調用關係
      • 缺點:難以精確確認某個方法的熱度、易受到線程阻塞或外界影響而擾亂熱點探測
    • 基於計數器的熱點探測(Counter Based Hot Spot Detection):爲每個方法建立計數器來統計方法的執行次數,執行次數超過一定的閾值就是熱點方法
      • 優點:精確、嚴謹
      • 缺點:實現較麻煩、不能直接獲取到方法的調用關係
      • 計數器類型:
        • 方法調用計數器(Invocation Counter):統計方法被調用的次數,當計數器超過閾值會觸發JIT編譯
        • 回邊計數器(Back Edge Counter):統計方法中循環體代碼執行的次數,當計數器超過閾值會觸發OSR編譯

c.HotSpot即時編譯器的編譯過程

  • Client Compiler:主要進行局部優化、放棄耗時較長的全局優化。採用簡單快速的三段式編譯:
    • 第一個階段:一個平臺獨立的前端把字節碼構造成一種高級中間代碼表示(HIR),在此之前會在字節碼上完成一部分基礎優化,如方法內聯、常量傳播等。
    • 第二個階段:一個平臺相關的後端從HIR中產生低級中間代碼表示(LIR),在此之前會在HIR上完成另外一些優化,如空值檢查消除、範圍檢查消除等,以便讓HIR達到更高效的代碼表示形式。
    • 第三個階段:平臺相關的後端使用線性掃描算法在LIR 上分配寄存器,並在LIR上做窺孔優化,然後產生機器代碼。大致執行過程如圖:

  • Server Compiler:專門面向服務端的典型應用並且特別爲服務端的性能配置調整過,是一個充分優化過的高級編譯器,體現在:
    • 會執行所有經典的優化動作:如無用代碼消除、循環展開、循環表達式外提、消除公共子表達式、常量傳播、基本塊重排序等
    • 會實施與Java特性密切相關的優化技術:如範圍檢查消除、空值檢查消除等
    • 根據解釋器或Client Compiler提供的性能監控信息可能會進行一些不穩定的激進優化:如守護內聯、分支頻率預測等

另外,Server Compiler的寄存器分配器是一個全局圖着色分配器,能夠充分利用某些處理器架構上的大寄存器集合。雖然Server Compiler的編譯時間比較緩慢,但是其編譯速度遠超於傳統的靜態優化編譯器,且比Client Compiler編譯輸出的代碼質量更高,能減少本地代碼的執行時間,從而抵消了額外的編譯時間開銷。

d.HotSpot虛擬機即時編譯器在生成代碼時採用的代碼優化技術

其中幾種最有代表性的優化技術:

  • 語言無關的經典優化技術之一:公共子表達式消除(Common Subexpression Elimination)
    • 含義:若一個表達式E已經計算過且E中所有變量值未發生任何變化,則稱E爲公共子表達式,此時沒必要花時間再次計算,直接用之前計算過的表達式結果代替E即可
    • 類型:
      • 局部公共子表達式消除:優化僅限於程序的基本塊內
      • 全局公共子表達式消除:優化的範圍涵蓋了多個基本塊
  • 語言相關的經典優化技術之一 :數組邊界檢查消除(Array Bounds Checking Elimination)
    • 若數組下標是個常量,只要在編譯期根據數據流分析確定這個數組的長度,且判斷得出該數組下標未越界,那麼運行時無需再檢查
    • 若數組訪問發生在循環中且使用循環變量來進行數組訪問,只要在編譯期根據數據流分析確定循環變量的取值範圍永遠在區間[0,數組長度)內,那麼在整個循環中無需再進行多次檢查
  • 最重要的優化技術之一:方法內聯(Method Inlining)
    • 含義:把目標方法的代碼復
    • 制到發起調用的方法之中,避免發生真實的方法調用
    • 主要目的:去除方法調用的成本,如建立棧幀等;爲其他優化建立良好的基礎,便於在更大範圍上採取後續的優化手段、獲取更好的優化效果
  • 最前沿的優化技術之一:逃逸分析(Escape Analysis)
    • 基本行爲:分析對象動態作用域
    • 類型:
      • 方法逃逸:一個對象在方法中被定義後,可能被外部方法所引用。如作爲調用參數傳遞到其他方法中
      • 線程逃逸:一個對象在方法中被定義後,能被外部線程訪問到。如賦值給類變量或可以在其他線程中訪問的實例變量
    • 對能夠證明不會逃逸到方法或線程之外的對象可進行的優化手段:
      • 棧上分配(Stack Allocation):在棧上對該對象進行內存分配,此時該對象所佔用的內存空間會隨棧幀出棧而銷燬,可減少垃圾收集系統的壓力
      • 同步消除(Synchronization Elimination):在該對象上不會有讀寫競爭,可消除掉對該對象的同步措施,從而減少資源的消耗
      • 標量替換(Scalar Replacement):若該對象可以進一步分解,那麼直接創建它的若干個被這個方法使用到的成員變量來替換

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章