jvm系列(九):如何優(yōu)化Java GC「譯」
本文由CrowHawk(https://crowhawk.github.io/2017/08/21/jvm_4/)翻譯,是Java GC調(diào)優(yōu)的經(jīng)典佳作。
本文翻譯自Sangmin Lee發(fā)表在Cubrid上的"Become a Java GC Expert"系列文章的第三篇《How to Tune Java Garbage Collection》,本文的作者是韓國人,寫在JDK 1.8發(fā)布之前,雖然有些地方有些許過時(shí),但整體內(nèi)容還是非常有價(jià)值的。譯者此前也看到有人翻譯了本文,發(fā)現(xiàn)其中有許多錯(cuò)漏生硬和語焉不詳之處,因此決定自己翻譯一份,供大家分享。
筆者將基于實(shí)際生產(chǎn)環(huán)境中的案例,介紹幾個(gè)GC優(yōu)化的***參數(shù)設(shè)置。在此我們假設(shè)你已經(jīng)理解了本系列前兩篇文章的內(nèi)容,因此為了更深入的理解本文所講內(nèi)容。
GC優(yōu)化是必要的嗎?
或者更準(zhǔn)確地說,GC優(yōu)化對(duì)Java基礎(chǔ)服務(wù)來說是必要的嗎?答案是否定的,事實(shí)上GC優(yōu)化對(duì)Java基礎(chǔ)服務(wù)來說在有些場合是可以省去的,但前提是這些正在運(yùn)行的Java系統(tǒng),必須包含以下參數(shù)或行為:
- 內(nèi)存大小已經(jīng)通過-Xms和-Xmx參數(shù)指定過
- 運(yùn)行在server模式下(使用-server參數(shù))
- 系統(tǒng)中沒有殘留超時(shí)日志之類的錯(cuò)誤日志
換句話說,如果你在運(yùn)行時(shí)沒有手動(dòng)設(shè)置內(nèi)存大小并且打印出了過多的超時(shí)日志,那你就需要對(duì)系統(tǒng)進(jìn)行GC優(yōu)化。
不過你需要時(shí)刻謹(jǐn)記一句話:GC tuning is the last task to be done.
現(xiàn)在來想一想GC優(yōu)化的最根本原因,垃圾收集器的工作就是清除Java創(chuàng)建的對(duì)象,垃圾收集器需要清理的對(duì)象數(shù)量以及要執(zhí)行的GC數(shù)量均取決于已創(chuàng)建的對(duì)象數(shù)量。因此,為了使你的系統(tǒng)在GC上表現(xiàn)良好,首先需要減少創(chuàng)建對(duì)象的數(shù)量。
俗話說“冰凍三尺非一日之寒”,我們?cè)诰幋a時(shí)要首先要把下面這些小細(xì)節(jié)做好,否則一些瑣碎的不良代碼累積起來將讓GC的工作變得繁重而難于管理:
使用 StringBuilder或 StringBuffer來代替 String盡量少輸出日志
盡管如此,仍然會(huì)有我們束手無策的情況。XML和JSON解析過程往往占用了最多的內(nèi)存,即使我們已經(jīng)盡可能地少用String、少輸出日志,仍然會(huì)有大量的臨時(shí)內(nèi)存(大約10-100MB)被用來解析XML或JSON文件,但我們又很難棄用XML和JSON。在此,你只需要知道這一過程會(huì)占據(jù)大量內(nèi)存即可。
如果在經(jīng)過幾次重復(fù)的優(yōu)化后應(yīng)用程序的內(nèi)存用量情況有所改善,那么久可以啟動(dòng)GC優(yōu)化了。
筆者總結(jié)了GC優(yōu)化的兩個(gè)目的:
- 將進(jìn)入老年代的對(duì)象數(shù)量降到***
- 減少Full GC的執(zhí)行時(shí)間
將進(jìn)入老年代的對(duì)象數(shù)量降到***
除了可以在JDK 7及更高版本中使用的G1收集器以外,其他分代GC都是由Oracle JVM提供的。關(guān)于分代GC,就是對(duì)象在Eden區(qū)被創(chuàng)建,隨后被轉(zhuǎn)移到Survivor區(qū),在此之后剩余的對(duì)象會(huì)被轉(zhuǎn)入老年代。也有一些對(duì)象由于占用內(nèi)存過大,在Eden區(qū)被創(chuàng)建后會(huì)直接被傳入老年代。老年代GC相對(duì)來說會(huì)比新生代GC更耗時(shí),因此,減少進(jìn)入老年代的對(duì)象數(shù)量可以顯著降低Full GC的頻率。你可能會(huì)以為減少進(jìn)入老年代的對(duì)象數(shù)量意味著把它們留在新生代,事實(shí)正好相反,新生代內(nèi)存的大小是可以調(diào)節(jié)的。
降低Full GC的時(shí)間
Full GC的執(zhí)行時(shí)間比Minor GC要長很多,因此,如果在Full GC上花費(fèi)過多的時(shí)間(超過1s),將可能出現(xiàn)超時(shí)錯(cuò)誤。
- 如果通過減小老年代內(nèi)存來減少Full GC時(shí)間,可能會(huì)引起 OutOfMemoryError或者導(dǎo)致Full GC的頻率升高。
- 另外,如果通過增加老年代內(nèi)存來降低Full GC的頻率,F(xiàn)ull GC的時(shí)間可能因此增加。
因此,你需要把老年代的大小設(shè)置成一個(gè)“合適”的值。
影響GC性能的參數(shù)
正如我在系列的***篇文章《理解Java GC》末尾提到的,不要幻想著“如果有人用他設(shè)置的GC參數(shù)獲取了不錯(cuò)的性能,我們?yōu)槭裁床粡?fù)制他的參數(shù)設(shè)置呢?”,因?yàn)閷?duì)于不用的Web服務(wù),它們創(chuàng)建的對(duì)象大小和生命周期都不相同。
舉一個(gè)簡單的例子,如果一個(gè)任務(wù)的執(zhí)行條件是A,B,C,D和E,另一個(gè)完全相同的任務(wù)執(zhí)行條件只有A和B,那么哪一個(gè)任務(wù)執(zhí)行速度更快呢?作為常識(shí)來講,答案很明顯是后者。
Java GC參數(shù)的設(shè)置也是這個(gè)道理,設(shè)置好幾個(gè)參數(shù)并不會(huì)提升GC執(zhí)行的速度,反而會(huì)使它變得更慢。GC優(yōu)化的基本原則是將不同的GC參數(shù)應(yīng)用到兩個(gè)及以上的服務(wù)器上然后比較它們的性能,然后將那些被證明可以提高性能或減少GC執(zhí)行時(shí)間的參數(shù)應(yīng)用于最終的工作服務(wù)器上。
下面這張表展示了與內(nèi)存大小相關(guān)且會(huì)影響GC性能的GC參數(shù)
筆者在進(jìn)行GC優(yōu)化時(shí)最常用的參數(shù)是 -Xms, -Xmx和 -XX:NewRatio。 -Xms和 -Xmx參數(shù)通常是必須的,所以 NewRatio的值將對(duì)GC性能產(chǎn)生重要的影響。
有些人可能會(huì)問如何設(shè)置***代內(nèi)存大小,你可以用 -XX:PermSize和 -XX:MaxPermSize參數(shù)來進(jìn)行設(shè)置,但是要記住,只有當(dāng)出現(xiàn) OutOfMemoryError錯(cuò)誤時(shí)你才需要去設(shè)置***代內(nèi)存。
還有一個(gè)會(huì)影響GC性能的因素是垃圾收集器的類型,下表展示了關(guān)于GC類型的可選參數(shù)(基于JDK 6.0):
除了G1收集器外,可以通過設(shè)置上表中每種類型***行的參數(shù)來切換GC類型,最常見的非侵入式GC就是Serial GC,它針對(duì)客戶端系統(tǒng)進(jìn)行了特別的優(yōu)化。
會(huì)影響GC性能的參數(shù)還有很多,但是上述的參數(shù)會(huì)帶來最顯著的效果,請(qǐng)切記,設(shè)置太多的參數(shù)并不一定會(huì)提升GC的性能。
GC優(yōu)化的過程
GC優(yōu)化的過程和大多數(shù)常見的提升性能的過程相似,下面是筆者使用的流程:
1.監(jiān)控GC狀態(tài)
你需要監(jiān)控GC從而檢查系統(tǒng)中運(yùn)行的GC的各種狀態(tài),具體方法請(qǐng)查看系列的第二篇文章《如何監(jiān)控Java GC》
2.分析監(jiān)控結(jié)果后決定是否需要優(yōu)化GC
在檢查GC狀態(tài)后,你需要分析監(jiān)控結(jié)構(gòu)并決定是否需要進(jìn)行GC優(yōu)化。如果分析結(jié)果顯示運(yùn)行GC的時(shí)間只有0.1-0.3秒,那么就不需要把時(shí)間浪費(fèi)在GC優(yōu)化上,但如果運(yùn)行GC的時(shí)間達(dá)到1-3秒,甚至大于10秒,那么GC優(yōu)化將是很有必要的。
但是,如果你已經(jīng)分配了大約10GB內(nèi)存給Java,并且這些內(nèi)存無法省下,那么就無法進(jìn)行GC優(yōu)化了。在進(jìn)行GC優(yōu)化之前,你需要考慮為什么你需要分配這么大的內(nèi)存空間,如果你分配了1GB或2GB大小的內(nèi)存并且出現(xiàn)了 OutOfMemoryError,那你就應(yīng)該執(zhí)行堆轉(zhuǎn)儲(chǔ)(heap dump)來消除導(dǎo)致異常的原因。
注意:
堆轉(zhuǎn)儲(chǔ)(heap dump)是一個(gè)用來檢查Java內(nèi)存中的對(duì)象和數(shù)據(jù)的內(nèi)存文件。該文件可以通過執(zhí)行JDK中的 jmap命令來創(chuàng)建。在創(chuàng)建文件的過程中,所有Java程序都將暫停,因此,不要再系統(tǒng)執(zhí)行過程中創(chuàng)建該文件。
你可以在互聯(lián)網(wǎng)上搜索heap dump的詳細(xì)說明。對(duì)于韓國讀者,可以直接參考我去年發(fā)布的書:《The story of troubleshooting for Java developers and system operators》 (Sangmin Lee, Hanbit Media, 2011, 416 pages)
3.設(shè)置GC類型/內(nèi)存大小
如果你決定要進(jìn)行GC優(yōu)化,那么你需要選擇一個(gè)GC類型并且為它設(shè)置內(nèi)存大小。此時(shí)如果你有多個(gè)服務(wù)器,請(qǐng)如上文提到的那樣,在每臺(tái)機(jī)器上設(shè)置不同的GC參數(shù)并分析它們的區(qū)別。
4.分析結(jié)果
在設(shè)置完GC參數(shù)后就可以開始收集數(shù)據(jù),請(qǐng)?jiān)谑占辽?4小時(shí)后再進(jìn)行結(jié)果分析。如果你足夠幸運(yùn),你可能會(huì)找到系統(tǒng)的***GC參數(shù)。如若不然,你還需要分析輸出日志并檢查分配的內(nèi)存,然后需要通過不斷調(diào)整GC類型/內(nèi)存大小來找到系統(tǒng)的***參數(shù)。
5.如果結(jié)果令人滿意,將參數(shù)應(yīng)用到所有服務(wù)器上并結(jié)束GC優(yōu)化
如果GC優(yōu)化的結(jié)果令人滿意,就可以將參數(shù)應(yīng)用到所有服務(wù)器上,并停止GC優(yōu)化。
在下面的章節(jié)中,你將會(huì)看到上述每一步所做的具體工作。
監(jiān)控GC狀態(tài)并分析結(jié)果
在運(yùn)行中的Web應(yīng)用服務(wù)器(Web Application Server,WAS)上查看GC狀態(tài)的***方式就是使用 jstat命令。筆者在《如何監(jiān)控Java GC》中已經(jīng)介紹過了 jstat命令,所以在本篇文章中我將著重關(guān)注數(shù)據(jù)部分。
下面的例子展示了某個(gè)還沒有執(zhí)行GC優(yōu)化的JVM的狀態(tài)(雖然它并不是運(yùn)行服務(wù)器)。
- $ jstat -gcutil 21719 1s
- S0 S1 E O P YGC YGCT FGC FGCT GCT
- 48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673
- 48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673
我們先看一下YGC(從應(yīng)用程序啟動(dòng)到采樣時(shí)發(fā)生 Young GC 的次數(shù))和YGCT(從應(yīng)用程序啟動(dòng)到采樣時(shí) Young GC 所用的時(shí)間(秒)),計(jì)算YGCT/YGC會(huì)得出,平均每次新生代的GC耗時(shí)50ms,這是一個(gè)很小的數(shù)字,通過這個(gè)結(jié)果可以看出,我們大可不必關(guān)注新生代GC對(duì)GC性能的影響。
現(xiàn)在來看一下FGC( 從應(yīng)用程序啟動(dòng)到采樣時(shí)發(fā)生 Full GC 的次數(shù))和FGCT(從應(yīng)用程序啟動(dòng)到采樣時(shí) Full GC 所用的時(shí)間(秒)),計(jì)算FGCT/FGC會(huì)得出,平均每次老年代的GC耗時(shí)19.68s。有可能是執(zhí)行了三次Full GC,每次耗時(shí)19.68s,也有可能是有兩次只花了1s,另一次花了58s。不管是哪一種情況,GC優(yōu)化都是很有必要的。
使用 jstat命令可以很容易地查看GC狀態(tài),但是分析GC的***方式是加上 -verbosegc參數(shù)來生成日志。在之前的文章中筆者已經(jīng)解釋了如何分析這些日志。HPJMeter是筆者最喜歡的用于分析 -verbosegc生成的日志的工具,它簡單易用,使用HPJmeter可以很容易地查看GC執(zhí)行時(shí)間以及GC發(fā)生頻率。
此外,如果GC執(zhí)行時(shí)間滿足下列所有條件,就沒有必要進(jìn)行GC優(yōu)化了:
- Minor GC執(zhí)行非常迅速(50ms以內(nèi))
- Minor GC沒有頻繁執(zhí)行(大約10s執(zhí)行一次)
- Full GC執(zhí)行非常迅速(1s以內(nèi))
- Full GC沒有頻繁執(zhí)行(大約10min執(zhí)行一次)
括號(hào)中的數(shù)字并不是絕對(duì)的,它們也隨著服務(wù)的狀態(tài)而變化。有些服務(wù)可能要求一次Full GC在0.9s以內(nèi),而有些則會(huì)放得更寬一些。因此,對(duì)于不同的服務(wù),需要按照不同的標(biāo)準(zhǔn)考慮是否需要執(zhí)行GC優(yōu)化。
當(dāng)檢查GC狀態(tài)時(shí),不能只查看Minor GC和Full GC的時(shí)間,還必須要關(guān)注GC執(zhí)行的次數(shù)。如果新生代空間太小,Minor GC將會(huì)非常頻繁地執(zhí)行(有時(shí)每秒會(huì)執(zhí)行一次,甚至更多)。此外,傳入老年代的對(duì)象數(shù)目會(huì)上升,從而導(dǎo)致Full GC的頻率升高。因此,在執(zhí)行 jstat命令時(shí),請(qǐng)使用 -gccapacity參數(shù)來查看具體占用了多少空間。
設(shè)置GC類型/內(nèi)存大小
設(shè)置GC類型
Oracle JVM有5種垃圾收集器,但是在JDK 7以前的版本中,你只能在Parallel GC, Parallel Compacting GC 和CMS GC之中選擇,至于具體選擇哪個(gè),則沒有具體的原則和規(guī)則。
既然這樣的話,我們?nèi)绾蝸磉x擇GC呢?***的方法是把三種都用上,但是有一點(diǎn)必須明確——CMS GC通常比其他并行(Parallel)GC都要快(這是因?yàn)镃MS GC是并發(fā)的GC),如果確實(shí)如此,那只選擇CMS GC就可以了,不過CMS GC也不總是更快,當(dāng)出現(xiàn)concurrent mode failure時(shí),CMS GC就會(huì)比并行GC更慢了。
Concurrent mode failure
現(xiàn)在讓我們來深入地了解一下concurrent mode failure。
并行GC和CMS GC的***區(qū)別是并行GC采用“標(biāo)記-整理”(Mark-Compact)算法而CMS GC采用“標(biāo)記-清除”(Mark-Sweep)算法(具體內(nèi)容可參照譯者的文章《GC算法與內(nèi)存分配策略》),compact步驟就是通過移動(dòng)內(nèi)存來消除內(nèi)存碎片,從而消除分配的內(nèi)存之間的空白區(qū)域。
對(duì)于并行GC來說,無論何時(shí)執(zhí)行Full GC,都會(huì)進(jìn)行compact工作,這消耗了太多的時(shí)間。不過在執(zhí)行完Full GC后,下次內(nèi)存分配將會(huì)變得更快(因?yàn)橹苯禹樞蚍峙湎噜彽膬?nèi)存)。
相反,CMS GC沒有compact的過程,因此CMS GC運(yùn)行的速度更快。但是也是由于沒有整理內(nèi)存,在進(jìn)行磁盤清理之前,內(nèi)存中會(huì)有很多零碎的空白區(qū)域,這也導(dǎo)致沒有足夠的空間分配給大對(duì)象。例如,在老年代還有300MB可用空間,但是連一個(gè)10MB的對(duì)象都沒有辦法被順序存儲(chǔ)在老年代中,在這種情況下,會(huì)報(bào)出“concurrent mode failure”的warning,然后系統(tǒng)執(zhí)行compact操作。但是CMS GC在這種情況下執(zhí)行的compact操作耗時(shí)要比并行GC高很多,并且這還會(huì)導(dǎo)致另一個(gè)問題,關(guān)于“concurrent mode failure”的詳細(xì)說明,可用參考Oracle工程師撰寫的《Understanding CMS GC Logs》。
綜上所述,你需要根據(jù)你的系統(tǒng)情況為其選擇一個(gè)最適合的GC類型。
每個(gè)系統(tǒng)都有最適合它的GC類型等著你去尋找,如果你有6臺(tái)服務(wù)器,我建議你每兩個(gè)服務(wù)器設(shè)置相同的參數(shù),然后加上 -verbosegc參數(shù)再分析結(jié)果。
設(shè)置內(nèi)存大小
下面展示了內(nèi)存大小、GC運(yùn)行次數(shù)和GC運(yùn)行時(shí)間之間的關(guān)系:
大內(nèi)存空間
- 減少了GC的次數(shù)
- 提高了GC的運(yùn)行時(shí)間
小內(nèi)存空間
- 增多了GC的次數(shù)
- 降低了GC的運(yùn)行時(shí)間
關(guān)于如何設(shè)置內(nèi)存的大小,沒有一個(gè)標(biāo)準(zhǔn)答案,如果服務(wù)器資源充足并且Full GC能在1s內(nèi)完成,把內(nèi)存設(shè)為10GB也是可以的,但是大部分服務(wù)器并不處在這種狀態(tài)中,當(dāng)內(nèi)存設(shè)為10GB時(shí),F(xiàn)ull GC會(huì)耗時(shí)10-30s,具體的時(shí)間自然與對(duì)象的大小有關(guān)。
既然如此,我們?cè)撊绾卧O(shè)置內(nèi)存大小呢?通常我推薦設(shè)為500MB,這不是說你要通過 -Xms500m和 -Xmx500m參數(shù)來設(shè)置WAS內(nèi)存。根據(jù)GC優(yōu)化之前的狀態(tài),如果Full GC后還剩余300MB的空間,那么把內(nèi)存設(shè)為1GB是一個(gè)不錯(cuò)的選擇(300MB(默認(rèn)程序占用)+ 500MB(老年代最小空間)+200MB(空閑內(nèi)存))。這意味著你需要為老年代設(shè)置至少500MB空間,因此如果你有三個(gè)運(yùn)行服務(wù)器,可以把它們的內(nèi)存分別設(shè)置為1GB,1.5GB,2GB,然后檢查結(jié)果。
理論上來說,GC執(zhí)行速度應(yīng)該遵循1GB> 1.5GB> 2GB,1GB內(nèi)存時(shí)GC執(zhí)行速度最快。然而,理論上的1GB內(nèi)存Full GC消耗1s、2GB內(nèi)存Full GC消耗2 s在現(xiàn)實(shí)里是無法保證的,實(shí)際的運(yùn)行時(shí)間還依賴于服務(wù)器的性能和對(duì)象大小。因此,***的方法是創(chuàng)建盡可能多的測量數(shù)據(jù)并監(jiān)控它們。
在設(shè)置內(nèi)存空間大小時(shí),你還需要設(shè)置一個(gè)參數(shù): NewRatio。 NewRatio的值是新生代和老年代空間大小的比例。如果 XX:NewRatio=1,則新生代空間:老年代空間=1:1,如果堆內(nèi)存為1GB,則新生代:老年代=500MB:500MB。如果 NewRatio等于2,則新生代:老年代=1:2,因此, NewRatio的值設(shè)置得越大,則老年代空間越大,新生代空間越小。
你可能會(huì)認(rèn)為把 NewRatio設(shè)為1會(huì)是***的選擇,然而事實(shí)并非如此,根據(jù)筆者的經(jīng)驗(yàn),當(dāng) NewRatio設(shè)為2或3時(shí),整個(gè)GC的狀態(tài)表現(xiàn)得更好。
完成GC優(yōu)化最快地方法是什么?答案是比較性能測試的結(jié)果。為了給每臺(tái)服務(wù)器設(shè)置不同的參數(shù)并監(jiān)控它們,***查看的是一或兩天后的數(shù)據(jù)。當(dāng)通過性能測試來進(jìn)行GC優(yōu)化時(shí),你需要在不同的測試時(shí)保證它們有相同的負(fù)載和運(yùn)行環(huán)境。然而,即使是專業(yè)的性能測試人員,想精確地控制負(fù)載也很困難,并且需要大量的時(shí)間準(zhǔn)備。因此,更加方便容易的方式是直接設(shè)置參數(shù)來運(yùn)行,然后等待運(yùn)行的結(jié)果(即使這需要消耗更多的時(shí)間)。
分析GC優(yōu)化的結(jié)果
在設(shè)置了GC參數(shù)和 -verbosegc參數(shù)后,可以使用tail命令確保日志被正確地生成。如果參數(shù)設(shè)置得不正確或日志未生成,那你的時(shí)間就被白白浪費(fèi)了。如果日志收集沒有問題的話,在收集一或兩天數(shù)據(jù)后再檢查結(jié)果。最簡單的方法是把日志從服務(wù)器移到你的本地PC上,然后用HPJMeter分析數(shù)據(jù)。
在分析結(jié)果時(shí),請(qǐng)關(guān)注下列幾點(diǎn)(這個(gè)優(yōu)先級(jí)是筆者根據(jù)自己的經(jīng)驗(yàn)擬定的,我認(rèn)為選取GC參數(shù)時(shí)應(yīng)考慮的最重要的因素是Full GC的運(yùn)行時(shí)間。):
- 單次Full GC運(yùn)行時(shí)間
- 單次Minor GC運(yùn)行時(shí)間
- Full GC運(yùn)行間隔
- Minor GC運(yùn)行間隔
- 整個(gè)Full GC的時(shí)間
- 整個(gè)Minor GC的運(yùn)行時(shí)間
- 整個(gè)GC的運(yùn)行時(shí)間
- Full GC的執(zhí)行次數(shù)
- Minor GC的執(zhí)行次數(shù)
找到***的GC參數(shù)是件非常幸運(yùn)的,然而在大多數(shù)時(shí)候,我們并不會(huì)如此幸運(yùn),在進(jìn)行GC優(yōu)化時(shí)一定要小心謹(jǐn)慎,因?yàn)楫?dāng)你試圖一次完成所有的優(yōu)化工作時(shí),可能會(huì)出現(xiàn) OutOfMemoryError錯(cuò)誤。
優(yōu)化案例
到目前為止,我們一直在從理論上介紹GC優(yōu)化,現(xiàn)在是時(shí)候?qū)⑦@些理論付諸實(shí)踐了,我們將通過幾個(gè)例子來更深入地理解GC優(yōu)化。
示例1
下面這個(gè)例子是針對(duì)Service S的優(yōu)化,對(duì)于最近剛開發(fā)出來的Service S,執(zhí)行Full GC需要消耗過多的時(shí)間。
現(xiàn)在看一下執(zhí)行 jstat-gcutil的結(jié)果
- S0 S1 E O P YGC YGCT FGC FGCT GCT
- 12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993
左邊的Perm區(qū)的值對(duì)于最初的GC優(yōu)化并不重要,而YGC參數(shù)的值更加對(duì)于這次優(yōu)化更為重要。
平均執(zhí)行一次Minor GC和Full GC消耗的時(shí)間如下表所示:
37ms對(duì)于Minor GC來說還不賴,但1.389s對(duì)于Full GC來說意味著當(dāng)GC發(fā)生在數(shù)據(jù)庫Timeout設(shè)置為1s的系統(tǒng)中時(shí),可能會(huì)頻繁出現(xiàn)超時(shí)現(xiàn)象。
首先,你需要檢查開始GC優(yōu)化前內(nèi)存的使用情況。使用 jstat-gccapacity命令可以檢查內(nèi)存用量情況。在筆者的服務(wù)器上查看到的結(jié)果如下:
- NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC
- 212992.0 212992.0 212992.0 21248.0 21248.0 170496.0 1884160.0 1884160.0 1884160.0 1884160.0 262144.0 262144.0 262144.0 262144.0 54 5
其中的關(guān)鍵值如下:
- 新生代內(nèi)存用量:212,992 KB
- 老年代內(nèi)存用量:1,884,160 KB
因此,除了***代以外,被分配的內(nèi)存空間加起來有2GB,并且新生代:老年代=1:9,為了得到比使用 jstat更細(xì)致的結(jié)果,還需加上 -verbosegc參數(shù)獲取日志,并把三臺(tái)服務(wù)器按照如下方式設(shè)置(除此以外沒有使用任何其他參數(shù)):
- NewRatio=2
- NewRatio=3
- NewRatio=4
一天后我得到了系統(tǒng)的GC log,幸運(yùn)的是,在設(shè)置完NewRatio后系統(tǒng)沒有發(fā)生任何Full GC。
這是為什么呢?這是因?yàn)榇蟛糠謱?duì)象在創(chuàng)建后很快就被回收了,所有這些對(duì)象沒有被傳入老年代,而是在新生代就被銷毀回收了。
在這樣的情況下,就沒有必要去改變其他的參數(shù)值了,只要選擇一個(gè)最合適的 NewRatio值即可。那么,如何確定***的NewRatio值呢?為此,我們分析一下每種 NewRatio值下Minor GC的平均響應(yīng)時(shí)間。
在每種參數(shù)下Minor GC的平均響應(yīng)時(shí)間如下:
- NewRatio=2:45ms
- NewRatio=3:34ms
- NewRatio=4:30ms
我們可以根據(jù)GC時(shí)間的長短得出NewRatio=4是***的參數(shù)值(盡管NewRatio=4時(shí)新生代空間是最小的)。在設(shè)置完GC參數(shù)后,服務(wù)器沒有發(fā)生Full GC。
為了說明這個(gè)問題,下面是服務(wù)執(zhí)行一段時(shí)間后執(zhí)行 jstat–gcutil的結(jié)果:
- S0 S1 E O P YGC YGCT FGC FGCT GCT
- 8.61 0.00 30.67 24.62 22.38 2424 30.219 0 0.000 30.219
你可能會(huì)認(rèn)為是服務(wù)器接收的請(qǐng)求少才使得GC發(fā)生的頻率較低,實(shí)際上,雖然Full GC沒有執(zhí)行過,但Minor GC被執(zhí)行了2424次。
示例2
這是一個(gè)Service A的例子。我們通過公司內(nèi)部的應(yīng)用性能管理系統(tǒng)(APM)發(fā)現(xiàn)JVM暫停了相當(dāng)長的時(shí)間(超過8秒),因此我們進(jìn)行了GC優(yōu)化。我們努力尋找JVM暫停的原因,后來發(fā)現(xiàn)是因?yàn)镕ull GC執(zhí)行時(shí)間過長,因此我們決定進(jìn)行GC優(yōu)化。
在GC優(yōu)化的開始階段,我們加上了 -verbosegc參數(shù),結(jié)果如下圖所示:
圖1:進(jìn)行GC優(yōu)化之前STW的時(shí)間
上圖是由HPJMeter生成的圖片之一。橫坐標(biāo)表示JVM執(zhí)行的時(shí)間,縱坐標(biāo)表示每次GC的時(shí)間。CMS為綠點(diǎn),表示Full GC的結(jié)果,而Parallel Scavenge為藍(lán)點(diǎn),表示Minor GC的結(jié)果。
之前我說過CMS GC是最快的GC,但是上面的結(jié)果顯示在一些時(shí)候CMS耗時(shí)達(dá)到了15s。是什么導(dǎo)致了這一結(jié)果?請(qǐng)記住我之前說的:CMS在執(zhí)行compact(整理)操作時(shí)會(huì)顯著變慢。此外,服務(wù)的內(nèi)存通過 -Xms1g和 =Xmx4g設(shè)置了,而分配的內(nèi)存只有4GB。
因此筆者將GC類型從CMS GC改為了Parallel GC,把內(nèi)存大小設(shè)為2GB,并把 NewRatio設(shè)為3。在執(zhí)行 jstat-gcutil幾小時(shí)后的結(jié)果如下:
- S0 S1 E O P YGC YGCT FGC FGCT GCT
- 0.00 30.48 3.31 26.54 37.01 226 11.131 4 11.758 22.890
Full GC的時(shí)間縮短了,變成了每次3s,跟15s比有了顯著提升。但是3s依然不夠快,為此筆者創(chuàng)建了以下6種情況:
- Case 1: -XX:+UseParallelGC-Xms1536m-Xmx1536m-XX:NewRatio=2
- Case 2: -XX:+UseParallelGC-Xms1536m-Xmx1536m-XX:NewRatio=3
- Case 3: -XX:+UseParallelGC-Xms1g-Xmx1g-XX:NewRatio=3
- Case 4: -XX:+UseParallelOldGC-Xms1536m-Xmx1536m-XX:NewRatio=2
- Case 5: -XX:+UseParallelOldGC-Xms1536m-Xmx1536m-XX:NewRatio=3
- Case 6: -XX:+UseParallelOldGC-Xms1g-Xmx1g-XX:NewRatio=3
上面哪一種情況最快?結(jié)果顯示,內(nèi)存空間越小,運(yùn)行結(jié)果最少。下圖展示了性能***的Case 6的結(jié)果圖,它的最慢響應(yīng)時(shí)間只有1.7s,并且響應(yīng)時(shí)間的平均值已經(jīng)被控制到了1s以內(nèi)。
圖2:Case 6的持續(xù)時(shí)間圖
基于上圖的結(jié)果,按照Case 6調(diào)整了GC參數(shù),但這卻導(dǎo)致每晚都會(huì)發(fā)生 OutOfMemoryError。很難解釋發(fā)生異常的具體原因,簡單地說,應(yīng)該是批處理程序?qū)е铝藘?nèi)存泄漏,我們正在解決相關(guān)的問題。
如果只對(duì)GC日志做一些短時(shí)間的分析就將相關(guān)參數(shù)部署到所有服務(wù)器上來執(zhí)行GC優(yōu)化,這將是非常危險(xiǎn)的。切記,只有當(dāng)你同時(shí)仔細(xì)分析服務(wù)的執(zhí)行情況和GC日志后,才能保證GC優(yōu)化沒有錯(cuò)誤地執(zhí)行。
在上文中,我們通過兩個(gè)GC優(yōu)化的例子來說明了GC優(yōu)化是怎樣執(zhí)行的。正如上文中提到的,例子中設(shè)置的GC參數(shù)可以設(shè)置在相同的服務(wù)器之上,但前提是他們具有相同的CPU、操作系統(tǒng)、JDK版本并且運(yùn)行著相同的服務(wù)。此外,不要把我使用的參數(shù)照搬到你的應(yīng)用上,它們可能在你的機(jī)器上并不能起到同樣良好的效果。
總結(jié)
筆者沒有執(zhí)行heap dump并分析內(nèi)存的詳細(xì)內(nèi)容,而是通過自己的經(jīng)驗(yàn)進(jìn)行GC優(yōu)化。精確地分析內(nèi)存可以得到更好的優(yōu)化效果,不過這種分析一般只適用于內(nèi)存使用量相對(duì)固定的場景。如果服務(wù)嚴(yán)重過載并占有了大量的內(nèi)存,則建議你根據(jù)之前的經(jīng)驗(yàn)進(jìn)行GC優(yōu)化。
筆者已經(jīng)在一些服務(wù)上設(shè)置了G1 GC參數(shù)并進(jìn)行了性能測試,但還沒有應(yīng)用于正式的生產(chǎn)環(huán)境。G1 GC的速度快于任何其他的GC類型,但是你必須要升級(jí)到JDK 7。此外,暫時(shí)還無法保證它的穩(wěn)定性,沒有人知道運(yùn)行時(shí)是否會(huì)出現(xiàn)致命的錯(cuò)誤,因此G1 GC暫時(shí)還不適合投入應(yīng)用。
等未來JDK 7真正穩(wěn)定了(這并不是說它現(xiàn)在不穩(wěn)定),并且WAS針對(duì)JDK 7進(jìn)行優(yōu)化后,G1 GC最終能按照預(yù)期的那樣來工作,等到那一天我們可能就不再需要GC優(yōu)化了。