1. 程式人生 > >你的Java程式碼對JIT編譯友好麼?

你的Java程式碼對JIT編譯友好麼?

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也存在著兩個小問題,有時候會顯得不是那麼方便:

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

  • Hotspot虛擬機器目前不能將結果輸出到單獨的檔案中,目前只能是以標準輸出的形式展示。

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

  • 確定程式中位於要處理的關鍵路徑上的方法。

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

  • 檢查這些方法確實出現在了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 -
c2 "java.lang.CharacterData00","
int getNumericValue
(int)",8352 "java.lang.
CharacterData00","
int toLowerCase(int)",13392 "
java.lang.CharacterData00","
int toUpperCase(int)",1307// ...
skipped outpu
t2 "java.lang.invoke.
DirectMethodHandle","private static
java.lang.invoke.LambdaForm
makePreparedLambdaForm(java.lang.
invoke.MethodType,int)",6131 "
java.lang.invoke.
InnerClassLambdaMetafactory","
private java.lang.Class spinInnerClass()
",497// ... more output ----

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

  • 方法的位元組碼已經改變。

  • 這些方法為新的方法。

我們看一下以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.tx
t"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結合使用並不困難,但卻提供了簡單且很有實際作用的一步,尤其是對於開發團隊打算研究其程式中即時編譯執行情況時。大多數情況下,在效能優化中,一個快速的分析可以幫助我們完成一些容易實現的目標。

相關推薦

Java程式碼JIT編譯友好

JIT編譯器是Java虛擬機器(以下簡稱JVM)中效率最高並且最重要的組成部分之一。但是很多的程式並沒有充分利用JIT的高效能優化能力,很多開發者甚至也並不清楚他們的程式有效利用JIT的程度。 在本文中,我們將介紹一些簡單的方法來驗證你的程式是否對JIT友好。這裡我們

notepad++編輯器寫Java程式碼,無法編譯出現錯誤:編碼GBK的不可對映字元

所報錯誤:編碼GBK的不可對映字元 解決方案:  1.英文版notepad++    選單:Configure --> Options --> JDK Tools --> Compiler

(2)Hadoop核心 -- java程式碼MapReduce的例子1

案例一:wordcount字數統計功能 1.1 先準備兩個txt檔案,並上傳到hdfs上 test1.txt hello zhangsan lisi nihao hai zhangsan nihao lisi x xiaoming test2.txt zha

(2)Hadoop核心-java程式碼MapReduce的操作

上一篇檔案介紹了java程式碼怎麼操作hdfs檔案的,hdfs理念“就是一切皆檔案”,我們現在搞定了怎麼使用java上傳下載等操作了接下來就要處理檔案了,hadoop的mapreduce模組。 一、Hadoop Map/Reduce框架        

[WebKit] JavaScriptCore解析--基礎篇(三)從指令碼程式碼JIT編譯程式碼實現

前面說了一些解析、生成ByteCode直至JIT的基本概念,下面是對照JavaScriptCore原始碼來大致瞭解它的實現。 從JS Script到Byte Code 首先說明Lexer, Parser和ByteCode的生成都是由ProgramExecutable初始化

java程式碼實現javac編譯功能

 (摘自 http://www.evget.com/zh-CN/Info/catalog/15816.html ) Java作為業界應用最為廣泛的語言之一,深得眾多軟體廠商和開發者的推崇,更是被包括Oracle在內的眾多JCP成員積極地推動發展。但是對於Java語言的深度理

Java程式碼加密與反編譯(一):利用混淆器工具proGuardjar包加密

Java 程式碼編譯後生成的 .class 中包含有原始碼中的所有資訊(不包括註釋),尤其是在其中儲存有除錯資訊的時候。所以一個按照正常方式編譯的 Java .class 檔案可以非常輕易地被反編譯。通常情況下,反編譯可以利用現有的工具jd-gui.exe或者jad.e

【深入Java虛擬機】之七:Javac編譯JIT編譯

p s ots 基本 關鍵字 目前 關註 script 和數 語言 轉載請註明出處:http://blog.csdn.net/ns_code/article/details/18009455 編譯過程 不論是物理機還是虛擬機,大部分的程序代碼從開始編譯到最終轉化

java確定用單例了嗎?

其它 safe 一個 with public 也會 sim data- 可見性 作為程序員這樣的特殊物種來說,都掌握了一種特殊能力就是編程思想,邏輯比較慎重,可是有時候總會忽略到一些細節,比方我,一直以來總認為Singleton是設計模式裏最簡單的,不用

android_反編譯java程式碼

下載dex2jar-2.0.zip找個最新版的下了就行 http://sourceforge.net/projects/dex2jar/files/ 這個工具用於將dex檔案轉換成jar檔案(dex檔案是什麼先不用考慮) 之後解壓這個dex2jar-2.0.zip 找一個自己寫好的程式 匯

Java 程式碼編譯和執行的整個過程

Java 位元組碼的執行是由 JVM 執行引擎來完成,流程圖如下所示: Java 程式碼編譯和執行的整個過程包含了以下三個重要的機制: Java 原始碼編譯機制 類載入機制 類執行機制 Java 原始碼編譯機制 Java 原始碼編譯由以下三個過程組成: 分析和輸入到符

java學習:Java程式碼編寫規範開發的重要性

本文從Java程式碼編寫的初期到結尾,做了一次整體的總結,希望對初學者有幫助。 一個錯誤的命名會很誤導人,不良的命名,對於閱讀程式碼的人來說很糾結。一個良好的命名對自己也有很大的幫助。 我個人命名的變數都比較長,一般是單詞的全稱,這樣程式碼讀起來易懂,有些縮寫你根本不知道它代表的單詞是

【小家javaJava中的執行緒池,真的用了嗎?(教用正確的姿勢使用執行緒池)

相關閱讀 【小家java】java5新特性(簡述十大新特性) 重要一躍 【小家java】java6新特性(簡述十大新特性) 雞肋升級 【小家java】java7新特性(簡述八大新特性) 不溫不火 【小家java】java8新特性(簡述十大新特性) 飽受讚譽 【小家java】java9

Java程式碼編譯過程簡述

  程式碼編譯是由Javac編譯器來完成, 這是由.java原始碼檔案轉為 .class二進位制位元組碼檔案的過程。 詳細過程:        原始碼檔案*.java -> 詞法分析器 -> tokens流 -&

java-說說javaee中的session的理解,是怎麼用session的?

在伺服器上,通過Session來區分每一個上網的使用者 使用者只要一連線到伺服器,則立刻分配一個Session使用者 Session主要方法: 1、伺服器上通過Session來分別不同的使用者 → Session ID 任何連線到伺服器上的使用者,伺服器都會為之分配唯

6個關於Java包裝類拆箱和裝箱的判斷題,能做幾個?

雖然 Java 語言是典型的面向物件程式語言,但其中的八種基本資料型別並不支援面向物件程式設計,基本型別的資料不具備“物件”的特性——不攜帶屬性、沒有方法可呼叫。為此,Java為每種基本資料型別分別設計了對應的類,稱之為包裝類(Wrapper Classes)。 裝箱(Box

jvmjava程式碼做了哪些優化

一:分為執行時優化,比如偏斜鎖,記憶體分配,tlab等。還有一些優化解釋執行的 編譯器優化:將熱點程式碼以方法為單位轉換成機器碼,直接執行在底層硬體之上。 鎖優化,內建方法 jit 編譯。 哪些手段探測這些優化手段: -XX:+PrintCompilation-XX:UnlockDiagnostic

簡化Java程式碼,讓工作更高效|語言

計算機專家在問題求解時非常重視表示式簡潔性的價值。Unix的先驅者Ken Thompson曾經說過非常著名的一句話:“丟棄1000行程式碼的那一天是我最有成效的一天之一。”這對於任何一個需要持續支援和維護的軟體專案來說,都是一個當之無愧的目標。早期的Lisp貢獻

Java程式碼實現HBase的基本操作

概覽 1.匯入jar包 2.測試 3.異常處理 首先將HBase搭建完成,然後啟動Zookeeper,Hadoop,HBase叢集 1.匯入jar包 準備: 1.CentOS7 2.Zookeeper叢集 3.Hadoop2.7.3叢集 4.hbase2.0.0

Java程式碼實現hive的基本操作

1.匯入jar包 在eclipse上新建java專案,並在專案下建個lib資料夾,然後將jar包放到lib中匯入專案 hive的lib下的 將其全部匯入到專案中 2.測試 在你要測試的hive的主機的/usr/tmp建個student檔案,裡面放入一些