1. 程式人生 > >JVM實用引數(二)引數分類和即時(JIT)編譯器診斷

JVM實用引數(二)引數分類和即時(JIT)編譯器診斷

在這個系列的第二部分,我來介紹一下HotSpot JVM提供的不同類別的引數。我同樣會討論一些關於JIT編譯器診斷的有趣引數。

JVM 引數分類

HotSpot JVM 提供了三類引數。第一類包括了標準引數。顧名思義,標準引數中包括功能和輸出的引數都是很穩定的,很可能在將來的JVM版本中不會改變。你可以用java命令(或者是用 java -help)檢索出所有標準引數。我們在第一部分中已經見到過一些標準引數,例如:-server。

第二類是X引數,非標準化的引數在將來的版本中可能會改變。所有的這類引數都以-X開始,並且可以用java -X來檢索。注意,不能保證所有引數都可以被檢索出來,其中就沒有-Xcomp。

第三類是包含XX引數(到目前為止最多的),它們同樣不是標準的,甚至很長一段時間內不被列出來(最近,這種情況有改變 ,我們將在本系列的第三部分中討論它們)。然而,在實際情況中X引數和XX引數並沒有什麼不同。X引數的功能是十分穩定的,然而很多XX引數仍在實驗當中(主要是JVM的開發者用於debugging和調優JVM自身的實現)。值的一讀的介紹非標準引數的文件 HotSpot JVM documentation,其中明確的指出XX引數不應該在不瞭解的情況下使用。這是真的,並且我認為這個建議同樣適用於X引數(同樣一些標準引數也是)。不管類別是什麼,在使用引數之前應該先了解它可能產生的影響。

用一句話來說明XX引數的語法。所有的XX引數都以”-XX:”開始,但是隨後的語法不同,取決於引數的型別。

  • 對於布林型別的引數,我們有”+”或”-“,然後才設定JVM選項的實際名稱。例如,-XX:+<name>用於啟用<name>選項,而-XX:-<name>用於登出選項。
  • 對於需要非布林值的引數,如string或者integer,我們先寫引數的名稱,後面加上”=”,最後賦值。例如,  -XX:<name>=<value>給<name>賦值<value>。

現在讓我們來看看JIT編譯方面的一些XX引數。

-XX:+PrintCompilation and -XX:+CITime

當一個Java應用執行時,非常容易檢視JIT編譯工作。通過設定-XX:+PrintCompilation,我們可以簡單的輸出一些關於從位元組碼轉化成原生代碼的編譯過程。我們來看一個服務端VM執行的例子:

$ java -server -XX:+PrintCompilation Benchmark
  1       java.lang.String::hashCode (64 bytes)
  2       java.lang.AbstractStringBuilder::stringSizeOfInt (21 bytes)
  3       java.lang.Integer::getChars (131 bytes)
  4       java.lang.Object::<init> (1 bytes)
---   n   java.lang.System::arraycopy (static)
  5       java.util.HashMap::indexFor (6 bytes)
  6       java.lang.Math::min (11 bytes)
  7       java.lang.String::getChars (66 bytes)
  8       java.lang.AbstractStringBuilder::append (60 bytes)
  9       java.lang.String::<init> (72 bytes)
 10       java.util.Arrays::copyOfRange (63 bytes)
 11       java.lang.StringBuilder::append (8 bytes)
 12       java.lang.AbstractStringBuilder::<init> (12 bytes)
 13       java.lang.StringBuilder::toString (17 bytes)
 14       java.lang.StringBuilder::<init> (18 bytes)
 15       java.lang.StringBuilder::append (8 bytes)
[...]
 29       java.util.regex.Matcher::reset (83 bytes)

每當一個方法被編譯,就輸出一行-XX:+PrintCompilation。每行都包含順序號(唯一的編譯任務ID)和已編譯方法的名稱和大小。因此,順序號1,代表編譯String類中的hashCode方法到原生程式碼的資訊。根據方法的型別和編譯任務列印額外的資訊。例如,本地的包裝方法前方會有”n”引數,像上面的System::arraycopy一樣。注意這樣的方法不會包含順序號和方法佔用的大小,因為它不需要編譯為原生代碼。同樣可以看到被重複編譯的方法,例如StringBuilder::append順序號為11和15。輸出在順序號29時停止 ,這表明在這個Java應用執行時總共需要編譯29個方法。

沒有官方的文件關於-XX:+PrintCompilation,但是這個描述是對於此引數比較好的。我推薦更深入學習一下。

JIT編譯器輸出幫助我們理解客戶端VM與服務端VM的一些區別。用服務端VM,我們的應用例子輸出了29行,同樣用客戶端VM,我們會得到55行。這看起來可能很怪,因為服務端VM應該比客戶端VM做了“更多”的編譯。然而,由於它們各自的預設設定,服務端VM在判斷方法是不是熱點和需不需要編譯時比客戶端VM觀察方法的時間更長。因此,在使用服務端VM時,一些潛在的方法會稍後編譯就不奇怪了。

通過另外設定-XX:+CITime,我們可以在JVM關閉時得到各種編譯的統計資訊。讓我們看一下一個特定部分的統計:

$ java -server -XX:+CITime Benchmark
[...]
Accumulated compiler times (for compiled methods only)
------------------------------------------------
  Total compilation time   :  0.178 s
    Standard compilation   :  0.129 s, Average : 0.004
    On stack replacement   :  0.049 s, Average : 0.024
[...]

總共用了0.178s(在29個編譯任務上)。這些,”on stack replacement”佔用了0.049s,即編譯的方法目前在堆疊上用去的時間。這種技術並不是簡單的實現效能顯示,實際上它是非常重要的。沒有”on stack replacement”,方法如果要執行很長時間(比如,它們包含了一個長時間執行的迴圈),它們執行時將不會被它們編譯過的副本替換。

再一次,客戶端VM與服務端VM的比較是非常有趣的。客戶端VM相應的資料表明,即使有55個方法被編譯了,但這些編譯總共用了只有0.021s。服務端VM做的編譯少但是用的時間卻比客戶端VM多。這個原因是,使用服務端VM在生成原生代碼時執行了更多的優化。

在本系列的第一部分,我們已經學了-Xint和-Xcomp引數。結合使用-XX:+PrintCompilation和-XX:+CITime,在這兩個情況下(校對者注,客戶端VM與服務端VM),我們能對JIT編譯器的行為有更好的瞭解。使用-Xint,-XX:+PrintCompilation在這兩種情況下會產生0行輸出。同樣的,使用-XX:+CITime時,證實在編譯上沒有花費時間。現在換用-Xcomp,輸出就完全不同了。在使用客戶端VM時會產生726行輸出,然後沒有更多的,這是因為每個相關的方法都被編譯了。使用服務端VM,我們甚至能得到993行輸出,這告訴我們更積極的優化被執行了。同樣,JVM 拆機(JVM teardown)時打印出的統計顯示了兩個VM的巨大不同。考慮服務端VM的執行:

$ java -server -Xcomp -XX:+CITime Benchmark
[...]
Accumulated compiler times (for compiled methods only)
------------------------------------------------
  Total compilation time   :  1.567 s
    Standard compilation   :  1.567 s, Average : 0.002
    On stack replacement   :  0.000 s, Average : -1.#IO
[...]

使用-Xcomp編譯用了1.567s,這是使用預設設定(即,混合模式)的10倍。同樣,應用程式的執行速度要比用混合模式的慢。相比較之下,客戶端VM使用-Xcomp編譯726個方法只用了0.208s,甚至低於使用-Xcomp的服務端VM。

補充一點,這裡沒有”on stack replacement”發生,因為每一個方法在第一次呼叫時被編譯了。損壞的輸出“Average: -1.#IO”(正確的是:0)再一次表明了,非標準化的輸出引數不是非常可靠。

-XX:+UnlockExperimentalVMOptions

有些時候當設定一個特定的JVM引數時,JVM會在輸出“Unrecognized VM option”後終止。如果發生了這種情況,你應該首先檢查你是否輸錯了引數。然而,如果引數輸入是正確的,並且JVM並不識別,你或許需要設定-XX:+UnlockExperimentalVMOptions 來解鎖引數。我不是非常清楚這個安全機制的作用,但我猜想這個引數如果不正確使用可能會對JVM的穩定性有影響(例如,他們可能會過多的寫入debug輸出的一些日誌檔案)。

有一些引數只是在JVM開發時用,並不實際用於Java應用。如果一個引數不能被 -XX:+UnlockExperimentalVMOptions 開啟,但是你真的需要使用它,此時你可以嘗試使用debug版本的JVM。對於Java 6 HotSpot JVM你可以從這裡找到

-XX:+LogCompilation and -XX:+PrintOptoAssembly

如果你在一個場景中發現使用 -XX:+PrintCompilation,不能夠給你足夠詳細的資訊,你可以使用 -XX:+LogCompilation把擴充套件的編譯輸出寫到“hotspot.log”檔案中。除了編譯方法的很多細節之外,你也可以看到編譯器執行緒啟動的任務。注意-XX:+LogCompilation 需要使用-XX:+UnlockExperimentalVMOptions來解鎖。

JVM甚至允許我們看到從位元組碼編譯生成到原生代碼。使用-XX:+PrintOptoAssembly,由編譯器執行緒生成的原生代碼被輸出並寫到“hotspot.log”檔案中。使用這個引數要求執行的服務端VM是debug版本。我們可以研究-XX:+PrintOptoAssembly的輸出,以至於瞭解JVM實際執行什麼樣的優化,例如,關於死程式碼的消除。一個非常有趣的文章提供了一個例子

關於XX引數的更多資訊

如果這篇文章勾起了你的興趣,你可以自己看一下HotSpot JVM的XX 引數。這裡是一個很好的起點