鍍金池/ 教程/ Java/ 參數(shù)分類(lèi)和即時(shí)(JIT)編譯器診斷
新生代垃圾回收
參數(shù)分類(lèi)和即時(shí)(JIT)編譯器診斷
打印所有 XX 參數(shù)及值
GC 日志
JVM 類(lèi)型以及編譯器模式
內(nèi)存調(diào)優(yōu)
CMS 收集器
吞吐量收集器

參數(shù)分類(lèi)和即時(shí)(JIT)編譯器診斷

作者: PATRICK PESCHLOW 原文地址 譯者:趙峰 校對(duì):許巧輝

在這個(gè)系列的第二部分,我來(lái)介紹一下 HotSpot JVM 提供的不同類(lèi)別的參數(shù)。我同樣會(huì)討論一些關(guān)于 JIT 編譯器診斷的有趣參數(shù)。

JVM 參數(shù)分類(lèi)

HotSpot JVM 提供了三類(lèi)參數(shù)。第一類(lèi)包括了標(biāo)準(zhǔn)參數(shù)。顧名思義,標(biāo)準(zhǔn)參數(shù)中包括功能和輸出的參數(shù)都是很穩(wěn)定的,很可能在將來(lái)的 JVM 版本中不會(huì)改變。你可以用 java 命令(或者是用 java -help)檢索出所有標(biāo)準(zhǔn)參數(shù)。我們?cè)诘谝徊糠种幸呀?jīng)見(jiàn)到過(guò)一些標(biāo)準(zhǔn)參數(shù),例如:-server。

第二類(lèi)是 X 參數(shù),非標(biāo)準(zhǔn)化的參數(shù)在將來(lái)的版本中可能會(huì)改變。所有的這類(lèi)參數(shù)都以 - X 開(kāi)始,并且可以用 java -X 來(lái)檢索。注意,不能保證所有參數(shù)都可以被檢索出來(lái),其中就沒(méi)有 - Xcomp。

第三類(lèi)是包含 XX 參數(shù)(到目前為止最多的),它們同樣不是標(biāo)準(zhǔn)的,甚至很長(zhǎng)一段時(shí)間內(nèi)不被列出來(lái)(最近,這種情況有改變 ,我們將在本系列的第三部分中討論它們)。然而,在實(shí)際情況中 X 參數(shù)和 XX 參數(shù)并沒(méi)有什么不同。X 參數(shù)的功能是十分穩(wěn)定的,然而很多 XX 參數(shù)仍在實(shí)驗(yàn)當(dāng)中(主要是 JVM 的開(kāi)發(fā)者用于 debugging 和調(diào)優(yōu) JVM 自身的實(shí)現(xiàn))。值的一讀的介紹非標(biāo)準(zhǔn)參數(shù)的文檔 HotSpot JVM documentation,其中明確的指出 XX 參數(shù)不應(yīng)該在不了解的情況下使用。這是真的,并且我認(rèn)為這個(gè)建議同樣適用于 X 參數(shù)(同樣一些標(biāo)準(zhǔn)參數(shù)也是)。不管類(lèi)別是什么,在使用參數(shù)之前應(yīng)該先了解它可能產(chǎn)生的影響。

用一句話來(lái)說(shuō)明 XX 參數(shù)的語(yǔ)法。所有的 XX 參數(shù)都以”-XX:” 開(kāi)始,但是隨后的語(yǔ)法不同,取決于參數(shù)的類(lèi)型。

對(duì)于布爾類(lèi)型的參數(shù),我們有”+” 或”-“,然后才設(shè)置 JVM 選項(xiàng)的實(shí)際名稱。例如,-XX:+ 用于激活 選項(xiàng),而 - XX:- 用于注銷(xiāo)選項(xiàng)。 對(duì)于需要非布爾值的參數(shù),如 string 或者 integer,我們先寫(xiě)參數(shù)的名稱,后面加上”=”,最后賦值。例如, -XX:= 賦值 。 現(xiàn)在讓我們來(lái)看看 JIT 編譯方面的一些 XX 參數(shù)。

-XX:+PrintCompilation and -XX:+CITime

當(dāng)一個(gè) Java 應(yīng)用運(yùn)行時(shí),非常容易查看 JIT 編譯工作。通過(guò)設(shè)置 - XX:+PrintCompilation,我們可以簡(jiǎn)單的輸出一些關(guān)于從字節(jié)碼轉(zhuǎn)化成本地代碼的編譯過(guò)程。我們來(lái)看一個(gè)服務(wù)端 VM 運(yùn)行的例子:

$ 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)

每當(dāng)一個(gè)方法被編譯,就輸出一行 - XX:+PrintCompilation。每行都包含順序號(hào)(唯一的編譯任務(wù) ID)和已編譯方法的名稱和大小。因此,順序號(hào) 1,代表編譯 String 類(lèi)中的 hashCode 方法到原生代碼的信息。根據(jù)方法的類(lèi)型和編譯任務(wù)打印額外的信息。例如,本地的包裝方法前方會(huì)有”n” 參數(shù),像上面的 System::arraycopy 一樣。注意這樣的方法不會(huì)包含順序號(hào)和方法占用的大小,因?yàn)樗恍枰幾g為本地代碼。同樣可以看到被重復(fù)編譯的方法,例如 StringBuilder::append 順序號(hào)為 11 和 15。輸出在順序號(hào) 29 時(shí)停止 ,這表明在這個(gè) Java 應(yīng)用運(yùn)行時(shí)總共需要編譯 29 個(gè)方法。

沒(méi)有官方的文檔關(guān)于 - XX:+PrintCompilation,但是這個(gè)描述是對(duì)于此參數(shù)比較好的。我推薦更深入學(xué)習(xí)一下。

JIT 編譯器輸出幫助我們理解客戶端 VM 與服務(wù)端 VM 的一些區(qū)別。用服務(wù)端 VM,我們的應(yīng)用例子輸出了 29 行,同樣用客戶端 VM,我們會(huì)得到 55 行。這看起來(lái)可能很怪,因?yàn)榉?wù)端 VM 應(yīng)該比客戶端 VM 做了 “更多” 的編譯。然而,由于它們各自的默認(rèn)設(shè)置,服務(wù)端 VM 在判斷方法是不是熱點(diǎn)和需不需要編譯時(shí)比客戶端 VM 觀察方法的時(shí)間更長(zhǎng)。因此,在使用服務(wù)端 VM 時(shí),一些潛在的方法會(huì)稍后編譯就不奇怪了。

通過(guò)另外設(shè)置 - XX:+CITime,我們可以在 JVM 關(guān)閉時(shí)得到各種編譯的統(tǒng)計(jì)信息。讓我們看一下一個(gè)特定部分的統(tǒng)計(jì):

$ 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.178 s(在 29 個(gè)編譯任務(wù)上)。這些,”on stack replacement” 占用了 0.049 s,即編譯的方法目前在堆棧上用去的時(shí)間。這種技術(shù)并不是簡(jiǎn)單的實(shí)現(xiàn)性能顯示,實(shí)際上它是非常重要的。沒(méi)有”on stack replacement”,方法如果要執(zhí)行很長(zhǎng)時(shí)間(比如,它們包含了一個(gè)長(zhǎng)時(shí)間運(yùn)行的循環(huán)),它們運(yùn)行時(shí)將不會(huì)被它們編譯過(guò)的副本替換。

再一次,客戶端 VM 與服務(wù)端 VM 的比較是非常有趣的??蛻舳?VM 相應(yīng)的數(shù)據(jù)表明,即使有 55 個(gè)方法被編譯了,但這些編譯總共用了只有 0.021 s。服務(wù)端 VM 做的編譯少但是用的時(shí)間卻比客戶端 VM 多。這個(gè)原因是,使用服務(wù)端 VM 在生成本地代碼時(shí)執(zhí)行了更多的優(yōu)化。

在本系列的第一部分,我們已經(jīng)學(xué)了 - Xint 和 - Xcomp 參數(shù)。結(jié)合使用 - XX:+PrintCompilation 和 - XX:+CITime,在這兩個(gè)情況下(校對(duì)者注,客戶端 VM 與服務(wù)端 VM),我們能對(duì) JIT 編譯器的行為有更好的了解。使用 - Xint,-XX:+PrintCompilation 在這兩種情況下會(huì)產(chǎn)生 0 行輸出。同樣的,使用 - XX:+CITime 時(shí),證實(shí)在編譯上沒(méi)有花費(fèi)時(shí)間?,F(xiàn)在換用 - Xcomp,輸出就完全不同了。在使用客戶端 VM 時(shí)會(huì)產(chǎn)生 726 行輸出,然后沒(méi)有更多的,這是因?yàn)槊總€(gè)相關(guān)的方法都被編譯了。使用服務(wù)端 VM,我們甚至能得到 993 行輸出,這告訴我們更積極的優(yōu)化被執(zhí)行了。同樣,JVM 拆機(jī) (JVM teardown) 時(shí)打印出的統(tǒng)計(jì)顯示了兩個(gè) VM 的巨大不同。考慮服務(wù)端 VM 的運(yùn)行:

$ 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.567 s,這是使用默認(rèn)設(shè)置(即,混合模式)的 10 倍。同樣,應(yīng)用程序的運(yùn)行速度要比用混合模式的慢。相比較之下,客戶端 VM 使用 - Xcomp 編譯 726 個(gè)方法只用了 0.208 s,甚至低于使用 - Xcomp 的服務(wù)端 VM。

補(bǔ)充一點(diǎn),這里沒(méi)有”on stack replacement” 發(fā)生,因?yàn)槊恳粋€(gè)方法在第一次調(diào)用時(shí)被編譯了。損壞的輸出 “Average: -1.#IO”(正確的是: 0)再一次表明了,非標(biāo)準(zhǔn)化的輸出參數(shù)不是非??煽?。

-XX:+UnlockExperimentalVMOptions

有些時(shí)候當(dāng)設(shè)置一個(gè)特定的 JVM 參數(shù)時(shí),JVM 會(huì)在輸出 “Unrecognized VM option” 后終止。如果發(fā)生了這種情況,你應(yīng)該首先檢查你是否輸錯(cuò)了參數(shù)。然而,如果參數(shù)輸入是正確的,并且 JVM 并不識(shí)別,你或許需要設(shè)置 - XX:+UnlockExperimentalVMOptions 來(lái)解鎖參數(shù)。我不是非常清楚這個(gè)安全機(jī)制的作用,但我猜想這個(gè)參數(shù)如果不正確使用可能會(huì)對(duì) JVM 的穩(wěn)定性有影響(例如,他們可能會(huì)過(guò)多的寫(xiě)入 debug 輸出的一些日志文件)。

有一些參數(shù)只是在 JVM 開(kāi)發(fā)時(shí)用,并不實(shí)際用于 Java 應(yīng)用。如果一個(gè)參數(shù)不能被 -XX:+UnlockExperimentalVMOptions 開(kāi)啟,但是你真的需要使用它,此時(shí)你可以嘗試使用 debug 版本的 JVM。

-XX:+LogCompilation and -XX:+PrintOptoAssembly

如果你在一個(gè)場(chǎng)景中發(fā)現(xiàn)使用 -XX:+PrintCompilation,不能夠給你足夠詳細(xì)的信息,你可以使用 -XX:+LogCompilation 把擴(kuò)展的編譯輸出寫(xiě)到 “hotspot.log” 文件中。除了編譯方法的很多細(xì)節(jié)之外,你也可以看到編譯器線程啟動(dòng)的任務(wù)。注意 - XX:+LogCompilation 需要使用 - XX:+UnlockExperimentalVMOptions 來(lái)解鎖。

JVM 甚至允許我們看到從字節(jié)碼編譯生成到本地代碼。使用 - XX:+PrintOptoAssembly,由編譯器線程生成的本地代碼被輸出并寫(xiě)到 “hotspot.log” 文件中。使用這個(gè)參數(shù)要求運(yùn)行的服務(wù)端 VM 是 debug 版本。我們可以研究 - XX:+PrintOptoAssembly 的輸出,以至于了解 JVM 實(shí)際執(zhí)行什么樣的優(yōu)化,例如,關(guān)于死代碼的消除。一個(gè)非常有趣的文章提供了一個(gè)例子

關(guān)于 XX 參數(shù)的更多信息

如果這篇文章勾起了你的興趣,你可以自己看一下 HotSpot JVM 的 XX 參數(shù)。這里是一個(gè)很好的起點(diǎn)。