转:什么是即时编译(JIT)!?OpenJDK HotSpot VM剖析
重點
- 應用程序可以選擇一個適當的即時編譯器來進行接近機器級的性能優化。
- 分層編譯由五層編譯構成。
- 分層編譯提供了極好的啟動性能,并指導編譯的下一層編譯器提供高性能優化。
- 提供即時編譯相關診斷信息的JVM開關。
- 像內聯化和向量化之類的優化進一步增強了性能。
OpenJDK HotSpot Java Virtual Machine被人親切地稱為Java虛擬機或JVM,由兩個主要組件構成:執行引擎和運行時。JVM和Java API組成Java運行環境,也稱為JRE。
在本文中,我們將探討執行引擎,特別是即時編譯,以及OpenJDK HotSpot VM的運行時優化。
JVM的執行引擎和運行時
執行引擎由兩個主要組件構成:垃圾回收器(它回收垃圾對象并提供自動的內存或堆管理))以及即時編譯器(它把字節碼轉換為可執行的機器碼)。在OpenJDK 8中,“分層的編譯器”是默認的服務端編譯器。HotSpot也可以通過禁用分層的編譯器(-XX:-TieredCompilation)仍然選擇不分層的服務端編譯器(也稱為“C2”)。我們接下來將了解這些編譯器的更多內容。
JVM的運行時掌控著類的加載、字節碼的驗證和其他以下列出的重要功能。其中一個功能是“解釋”,我們將馬上對其進行深入地探討。你可以點擊此處了解JVM運行時的更多內容。
相關廠商內容
通過探針技術,實現Java應用程序自我防護
新Java,新未來
針對容器化服務的分布式存儲實踐
分布式關系型數據庫架構探索
互聯網金融的性能微創新,給你奇思妙想!
相關贊助商
QCon全球軟件開發大會上海站,2016年10月20日-22日,上海寶華萬豪酒店,精彩內容搶先看!
自適應的即時編譯和運行時優化
JVM系統為Java的“一次編寫,隨處運行”的能力提供背后的支撐。一個Java程序一旦編譯成字節碼就可以通過JVM實例運行了。
OpenJDK HotSpot VM轉換字節碼為可通過“混合模式”執行的可執行的機器碼。使用“混合模式”,第一步是解釋,它使用一個描述表把字節碼轉換為匯編碼。這是個預定義的表,也稱為“模版表”,針對每個字節碼指令都有對應的匯編碼。
解釋在JVM啟動時開始,是字節碼最慢的執行形式。Java字節碼是平臺無關的,由它解釋編譯成可執行的機器碼,這種機器碼肯定是平臺相關的。為了 更快更有效(并適應潛在的平臺)地生成機器碼,運行時會啟動即時編譯器例如即時編譯器。即時編譯器是一個自適應優化器,針對已證明為性能關鍵的方法予以優 化。為了確定這些性能關鍵的方法,JVM會針對以下關鍵指標持續監控這些代碼:
- 方法進入計數,為每個方法分配一個調用計數器。
- 循環分支(一般稱為循環邊)計數,為每個已執行的循環分配一個計數器。
如果一個具體方法的方法進入計數和循環邊計數超過了由運行時設定的編譯臨界值,則認定它為性能關鍵的方法。運行時使用這些指標來判定這些方法本身或 其調用者是否是性能關鍵的方法。同樣,如果一個循環的循環分支計數超過了之前已經指定的臨界值(基于編譯臨界值),那么也會認定它為性能關鍵的。如果循環 邊計數超過它的臨界值,那么只有那個循環是編譯過的。針對循環的編譯優化被稱為棧上替換(OSR),因為JVM是在棧上替換編譯的代碼的。
OpenJDK HotSpot VM有兩個不同的編譯器,每個都有它自己的編譯臨界值:
分層編譯的五個層次
通過引進分層編譯,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提供了兩種不同層次的向量支持:
在第一種情況下,在內部循環的工作過程中配備的樁能為內部循環提供向量支持,而且這個內部循環可以通過向量指令進行優化和替換。這與內部函數是類似的。
在HotSpot VM中SLP支持的理論依據是MIT實驗室的一篇論文。目前,HotSpot VM只優化固定展開次數的目標數組,Vladimir Kozlov舉了以下一個示例,他是Oracle編譯團隊的資深成員,在各種編譯器優化作出了杰出貢獻,其中就包括自動向量化支持。
a[j] = b + c * z[i]
如上代碼展開之后就可以被自動向量化了。
逃逸分析
逃逸分析是自適應優化的另一個額外好處。為判定任何內存分配是否“逃逸”,逃逸分析(縮寫為EA)會將整個中間表示圖考慮進來。也就是說,任意內存分配是否不在下列之一:
如果已分配的對象不是逃逸的,編譯的方法和對象不作為參數傳遞,那么該內存分配就可以被移除了,這個域的值可以存儲在寄存器中。如果已分配的對象未逃逸已編譯的方法,但作為參數傳遞了,JVM仍然可以移除與該對象有關聯的鎖,當用它比對其他對象時可以使用優化的比對指令。
其他常見的優化
還有一些自適應即時編譯器一起帶來的一些其他的OpenJDK HotSpot VM優化:
引用:http://www.infoq.com/cn/articles/OpenJDK-HotSpot-What-the-JIT
?
轉載于:https://www.cnblogs.com/ASPNET2008/p/5837281.html
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的转:什么是即时编译(JIT)!?OpenJDK HotSpot VM剖析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 辨异 —— 冠词(定冠词、不定冠词、零冠
- 下一篇: c/c++小知识