你的 Java 代碼對 JIT 編譯友好么?

jopen 9年前發布 | 27K 次閱讀 Java

JIT編譯器是Java虛擬機(以下簡稱JVM)中效率最高并且最重要的組成部分之一。但是很多的程序并沒有充分利用JIT的高性能優化能力,很多開發者甚至也并不清楚他們的程序有效利用JIT的程度。

在本文中,我們將介紹一些簡單的方法來驗證你的程序是否對JIT友好。這里我們并不打算覆蓋諸如JIT編譯器工作原理這些細節。只是提供一些簡單基礎的檢測和方法來幫助你的代碼對JIT友好,進而得到優化。

JIT編譯的關鍵一點就是JVM會自動地監控正在被解釋器執行的方法。一旦某個方法被視為頻繁調用,這個方法就會被標記,進而編譯成本地機器指令。這些頻繁執行的方法的編譯由后臺的一個JVM線程來完成。在編譯完成之前,JVM會執行這個方法的解釋執行版本。一旦該方法編譯完成,JVM會使用將方法調度表中該方法的解釋的版本替換成編譯后的版本。

Hotspot虛擬機有很多JIT編譯優化的技術,但是其中最重要的一個優化技術就是內聯。在內聯的過程中,JIT編譯器有效地將一個方法的方法體提取到其調用者中,從而減少虛方法調用。舉個例子,看如下的代碼:

public int add(int x, int y) {
    return x + y;
}
int result = add(a, b);

當內聯發生之后,上述代碼會變成

int result = a + b;

上面的變量a和b替換了方法的參數,并且add方法的方法體已經復制到了調用者的區域。使用內聯可以為程序帶來很多好處,比如

  • 不會引起額外的性能損失

  • 減少指針的間接引用

  • 不需要對內聯方法進行虛方法查找

另外,通過將方法的實現復制到調用者中,JIT編譯器處理的代碼增多,使得后續的優化和更多的內聯成為可能。

內聯取決于方法的大小。缺省情況下,含有35個字節碼或更少的方法可以進行內聯操作。對于被頻繁調用的方法,臨界值可以達到325個字節。我們可以通過設置-XX:MaxInlineSize=# 選項來修改最大的臨界值,通過設置?XX:FreqInlineSize=#選項來修改頻繁調用的方法的臨界值。但是在沒有正確的分析的情況下,我們不應該修改這些配置。因為盲目地修改可能會對程序的性能帶來不可預料的影響。

由于內聯會對代碼的性能有大幅提升,因此讓盡可能多的方法達到內聯條件尤為重要。這里我們介紹一款叫做Jarscan的工具來幫助我們檢測程序中有多少方法是對內聯友好的。

Jarscan工具是分析JIT編譯的JITWatch開源工具套件中的一部分。和在運行時分析JIT日志的主工具不同,Jarscan是一款靜態分析jar文件的工具。該工具的輸出結果格式為CSV,結果中包含了超過頻繁調用方法臨界值的方法等信息。JITWatch和Jarscan是 AdoptOpenJDK工程的一部分,該工程由Chris Newland領導。

在使用Jarscan并得到分析結果之前,需要從AdoptOpenJDK Jenkins網站下載二進制工具(Java 7 工具Java 8 工具)。

運行很簡單,如下所示

./jarScan.sh <jars to analyse>

更多關于Jarscan的細節可以訪問AdoptOpenJDK wiki進行了解。

上面產生的報告對于開發團隊的開發工作很有幫助,根據報告結果,他們可以查找程序中是否包含了過大而不能JIT編譯的關鍵路徑方法。上面的操作依賴于手動執行。但是為了以后的自動化,可以開啟Java的-XX:+PrintCompilation 選項。開啟這個選項會生成如下的日志信息:

37    1      java.lang.String::hashCode (67 bytes)
124   2  s!  java.lang.ClassLoader::loadClass  (58 bytes)

其中,第一列表示從進程啟動到JIT編譯發生經過的時間,單位為毫秒。第二列表示的是編譯id,表明該方法正在被編譯(在Hotspot中一個方法可以多次去優化和再優化)。第三列表示的是附加的一些標志信息,比如s代表synchronized,!代表有異常處理。最后兩列分別代表正在編譯的方法名稱和該方法的字節大小。

關于PrintCompilation輸出的更多細節,Stephen Colebourne寫過一篇博客文章詳細介紹日志結果中各列的具體含義,感興趣的可以訪問這里閱讀。

PrintCompilation的輸出結果會提供運行時正在編譯的方法的信息,Jarscan工具的輸出結果可以告訴我們哪些方法不能進行JIT 編譯。結合兩者,我們就可以清楚地知道哪些方法進行了編譯,哪些沒有進行。另外,PrintCompilation選項可以在線上環境使用,因為開啟這個選項幾乎不會影響JIT編譯器的性能。

但是,PrintCompilation也存在著兩個小問題,有時候會顯得不是那么方便:

  1. 輸出的結果中未包含方法的簽名,如果存在重載方法,區分起來則比較困難。

  2. Hotspot虛擬機目前不能將結果輸出到單獨的文件中,目前只能是以標準輸出的形式展示。

上述的第二個問題的影響在于PrintCompilation的日志會和其他常用的日志混在一起。對于大多數服務器端程序來說,我們需要一個過濾進程來將PrintCompilation的日志過濾到一個獨立的日志中。最簡單的判斷一個方法否是JIT友好的途徑就是遵循下面這個簡單的步驟:

  1. 確定程序中位于要處理的關鍵路徑上的方法。

  2. 檢查這些方法沒有出現在Jarscan的輸出結果中。

  3. 檢查這些方法確實出現在了PrintCompilation的輸出結果中。

如果一個方法超過了內聯的臨界值,大多數情況下最常用的方法就是講這個重要的方法拆分成多個可以進行內聯的小方法,這樣修改之后通常會獲取更好的執行效率。但是對于所有的性能優化而言,優化之前的執行效率需要測量記錄,并且需要需要同優化后的數據進行對比之后,才能決定是否進行優化。為了性能優化而做出的改變不應該是盲目的。

幾乎所有的Java程序都依賴大量的提供關鍵功能的庫。Jarscan可以幫助我們檢測哪些庫或者框架的方法超過了內聯的臨界值。舉一個具體的例子,我們這里檢查JVM主要的運行時庫 rt.jar文件。

為了讓結果有點意思,我們分別比較Java 7 和Java 8,并查看這個庫的變化。在開始之前我們需要安裝Java 7 和 Java8 JDK。首先,我們分別運行Jarscan掃描各自的rt.jar文件,并得到用來后續分析的報告結果:

  $ ./jarScan.sh /Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk/Contents/Home/jre/lib/rt.jar
  > large_jre_methods_7u71.txt
    $ ./jarScan.sh /Library/Java/JavaVirtualMachines/jdk1.8.0_25.jdk/Contents/Home/jre/lib/rt.jar
  > large_jre_methods_8u25.txt

上述操作結束之后,我們得到兩個CSV文件,一個是JDK 7u71的結果,另一個是JDK 8u25。然后我們看一看不同的版本內聯情況有哪些變化。首先,一個最簡單的判斷驗證方式,看一看不同版本的JRE中有多少對JIT不友好的方法。

$ wc -l large_jre_methods_*
 3684 large_jre_methods_7u71.txt
 3576 large_jre_methods_8u25.txt

我們可以看到,相比Java 7,Java 8 少了100多個內聯不友好的方法。下面繼續深入研究,看看一些關鍵的包的變化。為了便于理解如何操作,我們再次介紹一下Jarscan的輸出結果。Jarscan的輸出結果有如下3個屬性組成:

 "<package>","<method name and signature>",<num of bytes>

了解了上述的格式,我們可以利用一些Unix文本處理的工具來研究報告結果。比如,我們想看一下Java 7 和 Java 8 這兩個版本中java.lang包下哪些方法變得內聯友好了:

 $ cat large_jre_methods_7u71.txt large_jre_methods_8u25.txt | grep -i
  ^\"java.lang | sort | uniq -c

上面的語句使用grep命令過濾出每份報告中以java.lang開頭的行,即只顯示位于包java.lang中的類的內聯不友好的方法。sort | uniq -c 是一個比較老的Unix小技巧,首先將講行信息進行排序(相同的信息將聚集到一起),然后對上面的排序數據進行去重操作。另外本命令還會統計一個當前行信息重復的次數,這個數據位于每一行信息的最開始部分。讓我們看一下上述命令的執行結果:

$ cat large_jre_methods_7u71.txt large_jre_methods_8u25.txt | grep -i ^\"java.lang | sort | uniq -c
2 "java.lang.CharacterData00","int getNumericValue(int)",835
2 "java.lang.CharacterData00","int toLowerCase(int)",1339
2 "java.lang.CharacterData00","int toUpperCase(int)",1307
// ... skipped output
2 "java.lang.invoke.DirectMethodHandle","private static java.lang.invoke.LambdaForm makePreparedLambdaForm(java.lang.invoke.MethodType,int)",613
1 "java.lang.invoke.InnerClassLambdaMetafactory","private java.lang.Class spinInnerClass()",497
// ... more output ----

報告中,以2(這是使用了uniq -c 對相同的信息計算數量的結果)最為起始的條目說明這些方法在Java 7 和Java 8 中起字節碼大小沒有改變。雖然這并不能完全肯定地說明這些方法的字節碼沒有改變,但通常我們也可以視為沒有改變。重復次數為1的方法有如下的情況:

a)方法的字節碼已經改變。

b)這些方法為新的方法。

我們看一下以1開始的行數據

    1 "java.lang.invoke.AbstractValidatingLambdaMetafactory","voidvalidateMetafactoryArgs()",864
    1 "java.lang.invoke.InnerClassLambdaMetafactory","privatejava.lang.Class spinInnerClass()",497
    1 "java.lang.reflect.Executable","java.lang.String
    sharedToGenericString(int,boolean)",329

上面三個對內聯不友好的方法全部來自Java 8,因此這屬于新方法的情況。前兩個方法與lamda表達式實現相關,第三個方法和反射子系統中繼承層級調整有關。在這里,這個改變就是在Java 8 中引入了方法和構造器可以繼承的通用基類。

最后,我們看一看JDK核心庫一些令人驚訝的特性:

  $ grep -i ^\"java.lang.String large_jre_methods_8u25.txt
  "java.lang.String","public java.lang.String[] split(java.lang.String,int)",326
  "java.lang.String","public java.lang.String toLowerCase(java.util.Locale)",431
  "java.lang.String","public java.lang.String toUpperCase(java.util.Locale)",439

從上面的日志我們可以了解到,即使是Java 8 中一些java.lang.String中一些關鍵的方法還是處于內聯不友好的狀態。尤其是toLowerCase和toUpperCase這兩個方法居然過大而無法內聯,著實讓人感到奇怪。但是,這兩個方法由于要處理UTF-8數據而不是簡單的ASCII數據,進而增加了方法的復雜性和大小,因而超過了內聯友好的臨界值。

對于性能要求較高并且確定只處理ASCII數據的程序,通常我們需要實現一個自己的StringUtils類。該類中包含一些靜態的方法來實現上述內聯不友好的方法的功能,但這些靜態方法既保持緊湊型又能到達內聯的要求。

上述我們討論的改進都是大部分基于靜態分析。除此之外,使用強大的JITWatch工具可以幫助我們更好地優化。JITWatch工具需要設置 -XX:+LogCompilation選項開啟日志打印。其打印出來的日志為XML格式,而非PrintCompilation簡單的文本輸出,并且這些日志比較大,通常會到達幾百MB。它會影響正在運行的程序(默認情況下主要來自日志輸出的影響),因此這個選項不適合在線上的生產環境使用。

PrintCompilation和Jarscan結合使用并不困難,但卻提供了簡單且很有實際作用的一步,尤其是對于開發團隊打算研究其程序中即時編譯執行情況時。大多數情況下,在性能優化中,一個快速的分析可以幫助我們完成一些容易實現的目標。

關于作者

Ben Evans是jClarity公司的CEO,jClarity是一家致力于Java和JVM性能分析研究的創業公司。除此之外他還是London Java Community的負責人之一并在Java Community Process Executive Committee有一席之地。他之前的項目有Google IPO性能測試,金融交易系統,90年代知名電影網站等。

來源:infoq.com/cn

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!