jit編譯器是java虛拟機(以下簡稱jvm)中效率最高并且最重要的組成部分之一。但是很多的程式并沒有充分利用jit的高性能優化能力,很多開發者甚至也并不清楚他們的程式有效利用jit的程度。
在本文中,我們将介紹一些簡單的方法來驗證你的程式是否對jit友好。這裡我們并不打算覆寫諸如jit編譯器工作原理這些細節。隻是提供一些簡單基礎的檢測和方法來幫助你的代碼對jit友好,進而得到優化。
jit編譯的關鍵一點就是jvm會自動地監控正在被解釋器執行的方法。一旦某個方法被視為頻繁調用,這個方法就會被标記,進而編譯成本地機器指令。 這些頻繁執行的方法的編譯由背景的一個jvm線程來完成。在編譯完成之前,jvm會執行這個方法的解釋執行版本。一旦該方法編譯完成,jvm會使用将方法 排程表中該方法的解釋的版本替換成編譯後的版本。
hotspot虛拟機有很多jit編譯優化的技術,但是其中最重要的一個優化技術就是内聯。在内聯的過程中,jit編譯器有效地将一個方法的方法體提取到其調用者中,進而減少虛方法調用。舉個例子,看如下的代碼:
當内聯發生之後,上述代碼會變成
上面的變量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的細節可以通路adoptopenjdk wiki進行了解。
上面産生的報告對于開發團隊的開發工作很有幫助,根據報告結果,他們可以查找程式中是否包含了過大而不能jit編譯的關鍵路徑方法。上面的操作依賴 于手動執行。但是為了以後的自動化,可以開啟java的-xx:+printcompilation 選項。開啟這個選項會生成如下的日志資訊:
其中,第一清單示從程序啟動到jit編譯發生經過的時間,機關為毫秒。第二清單示的是編譯id,表明該方法正在被編譯(在hotspot中一個方法 可以多次去優化和再優化)。第三清單示的是附加的一些标志資訊,比如s代表synchronized,!代表有異常處理。最後兩列分别代表正在編譯的方法 名稱和該方法的位元組大小。
關于printcompilation輸出的更多細節,stephen colebourne寫過一篇部落格文章詳細介紹日志結果中各列的具體含義,感興趣的可以通路這裡閱讀。
printcompilation的輸出結果會提供運作時正在編譯的方法的資訊,jarscan工具的輸出結果可以告訴我們哪些方法不能進行jit 編譯。結合兩者,我們就可以清楚地知道哪些方法進行了編譯,哪些沒有進行。另外,printcompilation選項可以線上上環境使用,因為開啟這個 選項幾乎不會影響jit編譯器的性能。
但是,printcompilation也存在着兩個小問題,有時候會顯得不是那麼友善:
輸出的結果中未包含方法的簽名,如果存在重載方法,區分起來則比較困難。
hotspot虛拟機目前不能将結果輸出到單獨的檔案中,目前隻能是以标準輸出的形式展示。
上述的第二個問題的影響在于printcompilation的日志會和其他常用的日志混在一起。對于大多數伺服器端程式來說,我們需要一個過濾進 程來将printcompilation的日志過濾到一個獨立的日志中。最簡單的判斷一個方法否是jit友好的途徑就是遵循下面這個簡單的步驟:
确定程式中位于要處理的關鍵路徑上的方法。
檢查這些方法沒有出現在jarscan的輸出結果中。
檢查這些方法确實出現在了printcompilation的輸出結果中。
如果一個方法超過了内聯的臨界值,大多數情況下最常用的方法就是講這個重要的方法拆分成多個可以進行内聯的小方法,這樣修改之後通常會擷取更好的執 行效率。但是對于所有的性能優化而言,優化之前的執行效率需要測量記錄,并且需要需要同優化後的資料進行對比之後,才能決定是否進行優化。為了性能優化而 做出的改變不應該是盲目的。
幾乎所有的java程式都依賴大量的提供關鍵功能的庫。jarscan可以幫助我們檢測哪些庫或者架構的方法超過了内聯的臨界值。舉一個具體的例子,我們這裡檢查jvm主要的運作時庫 rt.jar檔案。
為了讓結果有點意思,我們分别比較java 7 和java 8,并檢視這個庫的變化。在開始之前我們需要安裝java 7 和 java8 jdk。首先,我們分别運作jarscan掃描各自的rt.jar檔案,并得到用來後續分析的報告結果:
上述操作結束之後,我們得到兩個csv檔案,一個是jdk 7u71的結果,另一個是jdk 8u25。然後我們看一看不同的版本内聯情況有哪些變化。首先,一個最簡單的判斷驗證方式,看一看不同版本的jre中有多少對jit不友好的方法。
我們可以看到,相比java 7,java 8 少了100多個内聯不友好的方法。下面繼續深入研究,看看一些關鍵的包的變化。為了便于了解如何操作,我們再次介紹一下jarscan的輸出結果。jarscan的輸出結果有如下3個屬性組成:
了解了上述的格式,我們可以利用一些unix文本處理的工具來研究報告結果。比如,我們想看一下java 7 和 java 8 這兩個版本中java.lang包下哪些方法變得内聯友好了:
上面的語句使用grep指令過濾出每份報告中以java.lang開頭的行,即隻顯示位于包java.lang中的類的内聯不友好的方法。sort | uniq -c 是一個比較老的unix小技巧,首先将講行資訊進行排序(相同的資訊将聚集到一起),然後對上面的排序資料進行去重操作。另外本指令還會統計一個目前行信 息重複的次數,這個資料位于每一行資訊的最開始部分。讓我們看一下上述指令的執行結果:
報告中,以2(這是使用了uniq -c 對相同的資訊計算數量的結果)最為起始的條目說明這些方法在java 7 和java 8 中起位元組碼大小沒有改變。雖然這并不能完全肯定地說明這些方法的位元組碼沒有改變,但通常我們也可以視為沒有改變。重複次數為1的方法有如下的情況:
a)方法的位元組碼已經改變。
b)這些方法為新的方法。
我們看一下以1開始的行資料
上面三個對内聯不友好的方法全部來自java 8,是以這屬于新方法的情況。前兩個方法與lamda表達式實作相關,第三個方法和反射子系統中繼承層級調整有關。在這裡,這個改變就是在java 8 中引入了方法和構造器可以繼承的通用基類。
最後,我們看一看jdk核心庫一些令人驚訝的特性:
從上面的日志我們可以了解到,即使是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年代知名電影網站等。