JIT編譯器

1. 什麼是Just In Time編譯器?

Hot Spot 編譯

當 JVM 執行代碼時,它並不當即開始編譯代碼。這主要有兩個緣由:html

首先,若是這段代碼自己在未來只會被執行一次,那麼從本質上看,編譯就是在浪費精力。由於將代碼翻譯成 java 字節碼相對於編譯這段代碼並執行代碼來講,要快不少。java

當 然,若是一段代碼頻繁的調用方法,或是一個循環,也就是這段代碼被屢次執行,那麼編譯就很是值得了。所以,編譯器具備的這種權衡能力會首先執行解釋後的代 碼,而後再去分辨哪些方法會被頻繁調用來保證其自己的編譯。其實說簡單點,就是 JIT 在起做用,咱們知道,對於 Java 代碼,剛開始都是被編譯器編譯成字節碼文件,而後字節碼文件會被交由 JVM 解釋執行,因此能夠說 Java 自己是一種半編譯半解釋執行的語言。Hot Spot VM 採用了 JIT compile 技術,將運行頻率很高的字節碼直接編譯爲機器指令執行以提升性能,因此當字節碼被 JIT 編譯爲機器碼的時候,要說它是編譯執行的也能夠。也就是說,運行時,部分代碼可能由 JIT 翻譯爲目標機器指令(以 method 爲翻譯單位,還會保存起來,第二次執行就不用翻譯了)直接執行。性能優化

第二個緣由是最優化,當 JVM 執行某一方法或遍歷循環的次數越多,就會更加了解代碼結構,那麼 JVM 在編譯代碼的時候就作出相應的優化。服務器

我 們將在後面講解這些優化策略,這裏,先舉一個簡單的例子:咱們知道 equals() 這個方法存在於每個 Java Object 中(由於是從 Object class 繼承而來)並且常常被覆寫。當解釋器遇到 b = obj1.equals(obj2) 這樣一句代碼,它則會查詢 obj1 的類型從而得知到底運行哪個 equals() 方法。而這個動態查詢的過程從某種程度上說是很耗時的。架構

在主流商用JVM(HotSpot、J9)中,Java程序一開始是經過解釋器(Interpreter)進行解釋執行的。當JVM發現某個方法或代碼塊運行特別頻繁時,就會把這些代碼認定爲「熱點代碼(Hot Spot Code)」,而後JVM會把這些代碼編譯成與本地平臺相關的機器碼,並進行各類層次的優化,完成這個任務的編譯器稱爲:即時編譯器(Just In Time Compiler,JIT)性能

JIT編譯器是「動態編譯器」的一種,相對的「靜態編譯器」則是指的好比:C/C++的編譯器優化

JIT並非JVM的必須部分,JVM規範並無規定JIT必須存在,更沒有限定和指導JIT。可是,JIT性能的好壞、代碼優化程度的高低倒是衡量一款JVM是否優秀的最關鍵指標之一,也是虛擬機中最核心且最能體現虛擬機技術水平的部分。線程


2. 編譯器與解釋器

首先,不是全部JVM都採用編譯器和解釋器並存的架構,但主流商用虛擬機,都同時包含這兩部分。翻譯

2.1 配合過程

  1. 當程序須要迅速啓動而後執行的時候,解釋器能夠首先發揮做用,編譯器不運行從而省去編譯時間,當即執行程序server

  2. 在程序運行後,隨着時間的推移,編譯器逐漸發揮做用,把愈來愈多的代碼編譯成本地代碼以後,能夠得到更高的執行效率

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

  4. 同時,解釋器還能夠做爲編譯器(C2纔會激進優化)激進優化時的一個「逃生門」,讓編譯器根據機率選擇一些大多數時候都能提高運行速度的優化手段,當激進優化假設不成立。如:加載了新類後,類型繼承結構出現變化,出現「罕見陷阱(Uncommon Trap)」時,能夠經過逆優化(Deoptimization)退回到解釋狀態繼續執行 
    (部分沒有解釋器的虛擬機,也會採用不進行激進優化的C1編譯器擔任「逃生門」的角色)

    這裏寫圖片描述

2.2 解釋器 - Interpreter

Interpreter解釋執行class文件,好像JavaScript執行引擎同樣

特殊的例子:

  • 最先的Sun Classic VM只有Interpreter
  • BEA JRockit VM則只有Compiler,但它主要面向服務端應用,部署在其上的應用不重點關注啓動時間

2.3 編譯器 - Compiler

只說HotSpot JVM

1. C1和C2:

HotSpot虛擬機內置了兩個即時編譯器,分別稱爲Client Compiler和Server Compiler,習慣上將前者稱爲C1,後者稱爲C2

2. 使用C1仍是C2?

HotSpot默認採用解釋器和其中一個編譯器直接配合的方式工做,使用那個編譯器取決於虛擬機運行的模式,HotSpot會根據自身版本和宿主機器硬件性能自動選擇模式,用戶也可使用「-client」或」-server」參數去指定

  1. 混合模式(Mixed Mode) 
    默認的模式,如上面描述的這種方式就是mixed mode

  2. 解釋模式(Interpreted Mode) 
    可使用參數「-Xint」,在此模式下所有代碼解釋執行

  3. 編譯模式(Compiled Mode) 
    參數「-Xcomp」,此模式優先採用編譯,可是沒法編譯時也會解釋(在最新的HotSpot中此參數被取消)

    能夠看到,個人JVM如今是mixed mode 
    這裏寫圖片描述

重要:↓

在JDK1.7(1.7僅包括Server模式)以後,HotSpot就不是默認「採用解釋器和其中一個編譯器」配合的方式了,而是採用了分層編譯,分層編譯時C1和C2有可能同時工做


3. 分層編譯

3.1 爲何要分層編譯?

因爲編譯器compile本地代碼須要佔用程序時間,要編譯出優化程度更高的代碼所花費的時間可能更長,且此時解釋器還要替編譯器收集性能監控信息,這對解釋執行的速度也有影響

因此,爲了在程序啓動響應時間與運行效率之間達到最佳平衡,HotSpot在JDK1.6中出現了分層編譯(Tiered Compilation)的概念並在JDK1.7的Server模式JVM中做爲默認策略被開啓

3.2 編譯層 tier(或者叫級別)

分層編譯根據編譯器編譯、優化的規模與耗時,劃分了不一樣的編譯層次(不僅如下3種),包括:

  • 第0層,程序解釋執行(沒有編譯),解釋器不開啓性能監控功能,可觸發第1層編譯。

  • 第1層,也稱C1編譯,將字節碼編譯爲本地代碼,進行簡單、可靠的優化,若有必要將加入性能監控的邏輯

  • 第2層(或2層以上),也稱爲C2編譯,也是將字節碼編譯爲本地代碼,可是會啓用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化

實施分層編譯後,C1和C2將會同時工做,許多代碼會被屢次編譯,用C1獲取更高的編譯速度,用C2來獲取更好的編譯質量,且在解釋執行的時候解釋器也無須再承擔收集性能監控信息的任務


4. 編譯對象與觸發條件

1. 誰被編譯了?

編譯對象就是以前說的「熱點代碼」,它有兩類:

  1. 被屢次調用的方法 
    • 一個方法被屢次調用,理應稱爲熱點代碼,這種編譯也是虛擬機中標準的JIT編譯方式
  2. 被屢次執行的循環體 
    • 編譯動做由循環體出發,但編譯對象依然會以整個方法爲對象
    • 這種編譯方式因爲編譯發生在方法執行過程當中,所以形象的稱爲:棧上替換(On Stack Replacement- OSR編譯,即方法棧幀還在棧上,方法就被替換了)

2. 觸發條件

1. 綜述

上面的方法和循環體都說「屢次」,那麼多少算多?換個說法就是編譯的觸發條件。

判斷一段代碼是否是熱點代碼,是否是須要觸發JIT編譯,這樣的行爲稱爲:熱點探測(Hot Spot Detection),有幾種主流的探測方式:

  1. 基於計數器的熱點探測(Counter Based Hot Spot Detection) 
    虛擬機會爲每一個方法(或每一個代碼塊)創建計數器,統計執行次數,若是超過閥值那麼就是熱點代碼。缺點是維護計數器開銷。

  2. 基於採樣的熱點探測(Sample Based Hot Spot Detection) 
    虛擬機會週期性檢查各個線程的棧頂,若是某個方法常常出如今棧頂,那麼就是熱點代碼。缺點是不精確。

  3. 基於蹤影的熱點探測(Trace Based Hot Spot Detection) 
    Dalvik中的JIT編譯器使用這種方式

2. HotSpot

HotSpot使用的是第1種,所以它爲每一個方法準備了兩類計數器:方法調用計數器(Invocation Counter)和回邊計數器(Back Edge Counter)

  1. 方法計數器

    • 默認閥值,在Client模式下是1500次,Server是10000次,能夠經過參數「-XX:CompileThreshold」來設定

    • 當一個方法被調用時會首先檢查是否存在被JIT編譯過得版本,若是存在則使用此本地代碼來執行;若是不存在,則將方法計數器+1,而後判斷「方法計數器和回邊計數器之和」是否超過閥值,若是是則會向編譯器提交一個方法編譯請求

    • 默認狀況下,執行引擎並不會同步等待上面的編譯完成,而是會繼續解釋執行。當編譯完成後,此方法的調用入口地址會被系統自動改寫爲新的本地代碼地址

    • 還有一點,熱度是會衰減的,也就是說不是僅僅+,也會-,熱度衰減動做是在虛擬機的GC執行時順便進行的

  2. 回邊計數器

    • 回邊,顧名思義,只有執行到大括號」}」時纔算+1

    • 默認閥值,Client下13995,Server下10700

    • 它的調用邏輯和方法計數器差很少,只不過遇到回邊指令時+一、超過閥值時會提交OSR編譯請求以及這裏沒有熱度衰減


5. 編譯過程

編譯過程是在後臺線程(daemon)中完成的,能夠經過參數「-XX:-BackgroundCompilation」來禁止後臺編譯,但此時執行線程就會同步等待編譯完成纔會執行程序

  1. Client Compiler 
    C1編譯器是一個簡單快速的三段式編譯器,主要關注「局部性能優化」,放棄許多耗時較長的全局優化手段 
    過程:class -> 1. 高級中間代碼 -> 2. 低級中間代碼 -> 3. 機器代碼
  2. Server Compiler 
    C2是專門面向服務器應用的編譯器,是一個充分優化過的高級編譯器,幾乎能達到GNU C++編譯器使用-O2參數時的優化強度。

使用參數「-XX:+PrintCompilation」會讓虛擬機在JIT時把方法名稱打印出來,如圖: 
這裏寫圖片描述


6. Java和C/C++的編譯器對比

這裏不是比Java和C/C++誰快這種大坑問題,只是比較編譯器(我認爲開發效率上Java快,執行效率上C/C++快)

這種對比表明了經典的即時編譯器與靜態編譯期的對比,其實整體來講Java編譯器有優有劣。主要就是動態編譯時間壓力大能作的優化少,還要作一些動態校驗。而靜態編譯器沒法實現一些開發上頗有用的動態特性。

 

 

轉載自:https://www.cnblogs.com/insistence/p/5901457.html

相關文章
相關標籤/搜索