轉:什麼是即時編譯(JIT)!?OpenJDK HotSpot VM剖析

重點

  • 應用程序能夠選擇一個適當的即時編譯器來進行接近機器級的性能優化。
  • 分層編譯由五層編譯構成。
  • 分層編譯提供了極好的啓動性能,並指導編譯的下一層編譯器提供高性能優化。
  • 提供即時編譯相關診斷信息的JVM開關。
  • 像內聯化和向量化之類的優化進一步加強了性能。

OpenJDK HotSpot Java Virtual Machine被人親切地稱爲Java虛擬機或JVM,由兩個主要組件構成:執行引擎和運行時。JVM和Java API組成Java運行環境,也稱爲JRE。html

在本文中,咱們將探討執行引擎,特別是即時編譯,以及OpenJDK HotSpot VM的運行時優化。java

JVM的執行引擎和運行時

執行引擎由兩個主要組件構成:垃圾回收器(它回收垃圾對象並提供自動的內存或堆管理))以及即時編譯器(它把字節碼轉換爲可執行的機器碼)。在OpenJDK 8中,「分層的編譯器」是默認的服務端編譯器。HotSpot也能夠經過禁用分層的編譯器(-XX:-TieredCompilation)仍然選擇不分層的服務端編譯器(也稱爲「C2」)。咱們接下來將瞭解這些編譯器的更多內容。數據庫

JVM的運行時掌控着類的加載、字節碼的驗證和其餘如下列出的重要功能。其中一個功能是「解釋」,咱們將立刻對其進行深刻地探討。你能夠點擊此處瞭解JVM運行時的更多內容。數組

自適應的即時編譯和運行時優化

JVM系統爲Java的「一次編寫,隨處運行」的能力提供背後的支撐。一個Java程序一旦編譯成字節碼就能夠經過JVM實例運行了。oracle

OpenJDK HotSpot VM轉換字節碼爲可經過「混合模式」執行的可執行的機器碼。使用「混合模式」,第一步是解釋,它使用一個描述表把字節碼轉換爲彙編碼。這是個預約義的表,也稱爲「模版表」,針對每一個字節碼指令都有對應的彙編碼。app

解釋在JVM啓動時開始,是字節碼最慢的執行形式。Java字節碼是平臺無關的,由它解釋編譯成可執行的機器碼,這種機器碼確定是平臺相關的。爲了 更快更有效(並適應潛在的平臺)地生成機器碼,運行時會啓動即時編譯器例如即時編譯器。即時編譯器是一個自適應優化器,針對已證實爲性能關鍵的方法予以優 化。爲了肯定這些性能關鍵的方法,JVM會針對如下關鍵指標持續監控這些代碼:分佈式

  • 方法進入計數,爲每一個方法分配一個調用計數器。
  • 循環分支(通常稱爲循環邊)計數,爲每一個已執行的循環分配一個計數器。

若是一個具體方法的方法進入計數和循環邊計數超過了由運行時設定的編譯臨界值,則認定它爲性能關鍵的方法。運行時使用這些指標來斷定這些方法自己或 其調用者是不是性能關鍵的方法。一樣,若是一個循環的循環分支計數超過了以前已經指定的臨界值(基於編譯臨界值),那麼也會認定它爲性能關鍵的。若是循環 邊計數超過它的臨界值,那麼只有那個循環是編譯過的。針對循環的編譯優化被稱爲棧上替換(OSR),由於JVM是在棧上替換編譯的代碼的。

OpenJDK HotSpot VM有兩個不一樣的編譯器,每一個都有它本身的編譯臨界值:

  1. 客戶端或C1編譯器,它的編譯臨界值比較低,只是1500,這有助於減小啓動時間。
  1. 服務端或C2編譯器,它的編譯臨界值比較高,達到了10000,這有助於針對性能關鍵的方法生成高度優化的代碼,這些方法由應用的關鍵執行路徑來斷定是否屬於性能關鍵方法。

分層編譯的五個層次

經過引進分層編譯,OpenJDK HotSpot VM 用戶能夠經過使用服務端編譯器改進啓動時間得到好處。分層編譯有五個編譯層次。在第0層(解釋層)啓動,儀表在這一層提供了性能關鍵方法的信息。很快就會 到達第1層,簡單的C1(客戶端)編譯器,它來優化這段代碼。在第一層沒有性能優化的信息。下面來到第2層,在此只有少數方法是編譯過的(再提一下是經過 客戶端編譯器)。在第2層,爲這些少數方法針對進入次數和循環分支收集性能分析信息。第3層將會看到由客戶端編譯器編譯的全部方法及其所有性能優化信息, 最後的第4層只對C2自身有效,是服務端編譯器。

分層編譯器以及代碼緩存的效果

當使用客戶端編譯(第2層以前)時,代碼在啓動期間經過客戶端編譯器予以優化,此時關鍵執行路徑保持預熱。這有助於生成比解釋型代碼更好的性能優化信息。編譯的代碼存在在一個稱爲「代碼緩存」的緩存裏。代碼緩存有固定的大小,若是滿了,JVM將中止方法編譯。

分層編譯能夠針對每一層設定它本身的臨界值,好比-XX:Tier3MinInvocationThreshold, -XX:Tier3CompileThreshold, -XX:Tier3BackEdgeThreshold。第三層最低調用臨界值爲100。而未分層的C1的臨界值爲1500,與之對比你會發現會很是頻繁 地發生分層編譯,針對客戶端編譯的方法生成了更多的性能分析信息。因而用於分層編譯的代碼緩存必需要比用於不分層的代碼緩存大得多,因此在OpenJDK 中用於分層編譯的代碼緩存默認大小爲240MB,而用於不分層的代碼緩存大小默認只有48MB。

若是代碼緩存滿了,JVM將給出警告標識,鼓勵用戶使用 –XX:ReservedCodeCacheSize 選項去增長代碼緩存的大小。

理解編譯

爲了可視化什麼方法會在什麼時候獲得編譯,OpenJDK HotSpot VM提供了一個很是有用的命令行選項,叫作-XX:+PrintCompilation,它會報告何時代碼緩存滿了,以及何時編譯中止了。

舉例以下:

567  693 % !   3       org.h2.command.dml.Insert::insertRows @ 76 (513 bytes)
656  797  n    0       java.lang.Object::clone (native)  
779  835  s           4       java.lang.StringBuffer::append (13 bytes)

上面的輸出格式爲:

timestamp compilation-id flags tiered-compilation-level class:
method <@ osr_bci> code-size <deoptimization>

在此,

timestamp(時間戳) 是JVM開始啓動到此時的時間

compilation-id(編譯器id) 是內部的引用id

flags(標記) 能夠是如下其中一種:

%: is_osr_method (是否osr方法@ 針對OSR方法代表字節碼)

s: is_synchronized(是否同步的)

!: has_exception_handler(有異常處理器)

b: is_blocking(是否堵塞)

n: is_native(是否原生)

tiered-compilation(分層的編譯器) 表示當開啓了分層編譯時的編譯層

Method(方法) 將用如下格式表示類和方法 類名::方法

@osr_bci(osr字節碼索引) 是OSR中的字節碼索引

code-size(代碼大小) 字節碼總大小

deoptimization(逆優化)表示一個方法是不是逆優化,以及不會被調用或是殭屍方法(更多詳細內容請見「動態逆優化」一節)。

基於以上關鍵字,咱們能夠判定例子中的第一行

567  693 % !  3  org.h2.command.dml.Insert::insertRows @ 76 (513 bytes)

的timestamp是567,compilation-ide是693。該方法有個以「!」標明的異常處理器。咱們還能判定分層編譯處於第3層, 它是一個OSR方法(以「%」標識的),字節碼索引爲76。字節碼總大小爲513個字節。請注意513個字節是字節碼的大小而不是編譯碼的大小。

示例的第2行顯示:

656  797  n 0 java.lang.Object::clone (native) 

JVM使一個原生方法更容易調用,第3行是:

779  835  s 4 java.lang.StringBuffer::append (13 bytes)

顯示這個方法是在第4層編譯的且是同步的。

動態逆優化

咱們知道Java會作動態類加載,JVM在每次動態類加載時檢查內部依賴。當再也不須要一個以前優化過的方法時,OpenJDK HotSpot VM將執行該方法的動態逆優化。自適應優化有助於動態逆優化,換句話說,一個動態逆優化的代碼應恢復到它以前編譯層,或者轉到新的編譯層,以下圖所示。 (注意:當在命令行中開啓PrintCompilation時會輸出以下信息):

 573  704 2 org.h2.table.Table::fireAfterRow (17 bytes)
7963 2223 4 org.h2.table.Table::fireAfterRow (17 bytes)
7964  704 2 org.h2.table.Table::fireAfterRow (17 bytes) made not entrant
33547 704 2 org.h2.table.Table::fireAfterRow (17 bytes) made zombie

這個輸出顯示timestamp爲7963,fireAfterRow是在第4層編譯的。以後的timestamp是7964,以前在第2層編譯的fireAfterRow沒有進入。而後過了一下子,fireAfterRow標記爲殭屍,也就是說,以前的代碼被回收了。

理解內聯

自適應優化的最大一個好處是有能力內聯性能關鍵的方法。經過把調用替換爲實際的方法體,有助於規避調用這些關鍵方法的間接開銷。針對內聯有不少基於規模和調用臨界值的「協調」選項,內聯已經獲得了充分地研究和優化,幾乎已經挖掘出了最大的潛力。

若是你想投入時間看一下內聯決策,可使用一個叫作-XX:+PrintInlining的JVM診斷選項。在理解決策時PrintInlining會提供很大的幫助,示例以下:

@ 76 java.util.zip.Inflater::setInput (74 bytes) too big
@ 80 java.io.BufferedInputStream::getBufIfOpen (21 bytes) inline (hot)
@ 91 java.lang.System::arraycopy (0 bytes)   (intrinsic)
@ 2  java.lang.ClassLoader::checkName (43 bytes) callee is too large

在這裏你能看到該內聯的位置和被內聯的總字節數。有時你看到如「too big」或「callee is too large」的標籤,這代表由於已經超過臨界值因此未進行內聯。第3行的輸出信息顯示了一個「intrinsic」標籤,讓咱們在下一節詳細瞭解一下 intrinsics(內部函數)。

內部函數

一般OpenJDK HotSpot VM 即時編譯器將執行爲性能關鍵方法生成的代碼,但有時有些方法有很是公共的模式,好比java.lang.System::arraycopy,如前一節中PrintInlining輸出的結果。這些方法能夠獲得手工優化從而造成更好的性能,優化的代碼相似於擁有你的原生方法,但沒有間接開銷。這些內部函數能夠高效地內聯,就像JVM內聯常規方法同樣。

向量化

討論內部函數的時候,我喜歡強調一個經常使用的編譯優化,那就是向量化。向量化可用於任何潛在的平臺(處理器),能處理特殊的並行計算或向量指令,好比 「SIMD」指令(單指令、多數據)。SIMD和「向量化」有助於在較大的緩存行規模(64字節)數據量上進行數據層的並行操做。

HotSpot VM提供了兩種不一樣層次的向量支持:

  1. 爲計數的內部循環配備樁;
  2. 針對自動向量化的超字級並行(SLP)支持。

在第一種狀況下,在內部循環的工做過程當中配備的樁能爲內部循環提供向量支持,並且這個內部循環能夠經過向量指令進行優化和替換。這與內部函數是相似的。

在HotSpot VM中SLP支持的理論依據是MIT實驗室的一篇論文。目前,HotSpot VM只優化固定展開次數的目標數組,Vladimir Kozlov舉了如下一個示例,他是Oracle編譯團隊的資深成員,在各類編譯器優化做出了傑出貢獻,其中就包括自動向量化支持。

a[j] = b + c * z[i]

如上代碼展開以後就能夠被自動向量化了。

逃逸分析

逃逸分析是自適應優化的另外一個額外好處。爲斷定任何內存分配是否「逃逸」,逃逸分析(縮寫爲EA)會將整個中間表示圖考慮進來。也就是說,任意內存分配是否不在下列之一:

  1. 存儲到靜態域或外部對象的非靜態域
  2. 從方法中返回;
  3. 做爲參數傳遞到另外一個逃逸的方法

若是已分配的對象不是逃逸的,編譯的方法和對象不做爲參數傳遞,那麼該內存分配就能夠被移除了,這個域的值能夠存儲在寄存器中。若是已分配的對象未逃逸已編譯的方法,但做爲參數傳遞了,JVM仍然能夠移除與該對象有關聯的鎖,當用它比對其餘對象時可使用優化的比對指令。

其餘常見的優化

還有一些自適應即時編譯器一塊兒帶來的一些其餘的OpenJDK HotSpot VM優化:

  1. 範圍檢查消除——若是能保證數組索引永遠不會越界的話,那麼在JVM中就不必定必需要檢查索引的邊界錯誤了。
  1. 循環展開——經過展開循環有助於減小迭代次數。這能使JVM有能力去應用其餘常見的優化(好比循環向量化),不管哪裏須要。

引用:http://www.infoq.com/cn/articles/OpenJDK-HotSpot-What-the-JIT

相關文章
相關標籤/搜索