jvm系列(十):如何优化Java GC「译」
本文由CrowHawk翻譯,地址:如何優(yōu)化Java GC「譯」,是Java GC調(diào)優(yōu)的經(jīng)典佳作。
Sangmin Lee發(fā)表在Cubrid上的"Become a Java GC Expert"系列文章的第三篇《How to Tune Java Garbage Collection》,本文的作者是韓國(guó)人,寫在JDK 1.8發(fā)布之前,雖然有些地方有些許過(guò)時(shí),但整體內(nèi)容還是非常有價(jià)值的。譯者此前也看到有人翻譯了本文,發(fā)現(xiàn)其中有許多錯(cuò)漏生硬和語(yǔ)焉不詳之處,因此決定自己翻譯一份,供大家分享。
本文是“成為Java GC專家”系列文章的第三篇,在系列的第一篇文章《理解Java GC》中,我們了解到了不同GC算法的執(zhí)行過(guò)程、GC的工作原理、新生代和老年代的概念、JDK 7中你需要了解的5種GC類型以及每一種GC對(duì)性能的影響。
在系列的第二篇文章《如何監(jiān)控Java GC》中筆者已經(jīng)解釋了JVM進(jìn)行實(shí)時(shí)GC的原理、監(jiān)控GC的方法以及可以使這一過(guò)程更加迅速高效的工具。
在第三篇文章中,筆者將基于實(shí)際生產(chǎn)環(huán)境中的案例,介紹幾個(gè)GC優(yōu)化的最佳參數(shù)設(shè)置。在此我們假設(shè)你已經(jīng)理解了本系列前兩篇文章的內(nèi)容,因此為了更深入的理解本文所講內(nèi)容,我建議你在閱讀本篇文章之前先仔細(xì)閱讀這兩篇文章。
GC優(yōu)化是必要的嗎?
或者更準(zhǔn)確地說(shuō),GC優(yōu)化對(duì)Java基礎(chǔ)服務(wù)來(lái)說(shuō)是必要的嗎?答案是否定的,事實(shí)上GC優(yōu)化對(duì)Java基礎(chǔ)服務(wù)來(lái)說(shuō)在有些場(chǎng)合是可以省去的,但前提是這些正在運(yùn)行的Java系統(tǒng),必須包含以下參數(shù)或行為:
- 內(nèi)存大小已經(jīng)通過(guò)-Xms和-Xmx參數(shù)指定過(guò)
- 運(yùn)行在server模式下(使用-server參數(shù))
- 系統(tǒng)中沒(méi)有殘留超時(shí)日志之類的錯(cuò)誤日志
換句話說(shuō),如果你在運(yùn)行時(shí)沒(méi)有手動(dòng)設(shè)置內(nèi)存大小并且打印出了過(guò)多的超時(shí)日志,那你就需要對(duì)系統(tǒng)進(jìn)行GC優(yōu)化。
不過(guò)你需要時(shí)刻謹(jǐn)記一句話:GC tuning is the last task to be done.
現(xiàn)在來(lái)想一想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ù)量。
俗話說(shuō)“冰凍三尺非一日之寒”,我們?cè)诰幋a時(shí)要首先要把下面這些小細(xì)節(jié)做好,否則一些瑣碎的不良代碼累積起來(lái)將讓GC的工作變得繁重而難于管理:
- 使用StringBuilder或StringBuffer來(lái)代替String
- 盡量少輸出日志
盡管如此,仍然會(huì)有我們束手無(wú)策的情況。XML和JSON解析過(guò)程往往占用了最多的內(nèi)存,即使我們已經(jīng)盡可能地少用String、少輸出日志,仍然會(huì)有大量的臨時(shí)內(nèi)存(大約10-100MB)被用來(lái)解析XML或JSON文件,但我們又很難棄用XML和JSON。在此,你只需要知道這一過(guò)程會(huì)占據(jù)大量?jī)?nèi)存即可。
如果在經(jīng)過(guò)幾次重復(fù)的優(yōu)化后應(yīng)用程序的內(nèi)存用量情況有所改善,那么久可以啟動(dòng)GC優(yōu)化了。
筆者總結(jié)了GC優(yōu)化的兩個(gè)目的:
將進(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)存過(guò)大,在Eden區(qū)被創(chuàng)建后會(huì)直接被傳入老年代。老年代GC相對(duì)來(lái)說(shuō)會(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要長(zhǎng)很多,因此,如果在Full GC上花費(fèi)過(guò)多的時(shí)間(超過(guò)1s),將可能出現(xiàn)超時(shí)錯(cuò)誤。
- 如果通過(guò)減小老年代內(nèi)存來(lái)減少Full GC時(shí)間,可能會(huì)引起OutOfMemoryError或者導(dǎo)致Full GC的頻率升高。
- 另外,如果通過(guò)增加老年代內(nèi)存來(lái)降低Full GC的頻率,Full 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è)簡(jiǎn)單的例子,如果一個(gè)任務(wù)的執(zhí)行條件是A,B,C,D和E,另一個(gè)完全相同的任務(wù)執(zhí)行條件只有A和B,那么哪一個(gè)任務(wù)執(zhí)行速度更快呢?作為常識(shí)來(lái)講,答案很明顯是后者。
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ù)
表1:GC優(yōu)化需要考慮的JVM參數(shù)| 堆內(nèi)存大小 | -Xms | 啟動(dòng)JVM時(shí)堆內(nèi)存的大小 |
| -Xmx | 堆內(nèi)存最大限制 | |
| 新生代空間大小 | -XX:NewRatio | 新生代和老年代的內(nèi)存比 |
| -XX:NewSize | 新生代內(nèi)存大小 | |
| -XX:SurvivorRatio | Eden區(qū)和Survivor區(qū)的內(nèi)存比 |
筆者在進(jìn)行GC優(yōu)化時(shí)最常用的參數(shù)是-Xms,-Xmx和-XX:NewRatio。-Xms和-Xmx參數(shù)通常是必須的,所以NewRatio的值將對(duì)GC性能產(chǎn)生重要的影響。
有些人可能會(huì)問(wèn)如何設(shè)置永久代內(nèi)存大小,你可以用-XX:PermSize和-XX:MaxPermSize參數(shù)來(lái)進(jìn)行設(shè)置,但是要記住,只有當(dāng)出現(xiàn)OutOfMemoryError錯(cuò)誤時(shí)你才需要去設(shè)置永久代內(nèi)存。
還有一個(gè)會(huì)影響GC性能的因素是垃圾收集器的類型,下表展示了關(guān)于GC類型的可選參數(shù)(基于JDK 6.0):
表2:GC類型可選參數(shù)| Serial GC | -XX:+UseSerialGC | |
| Parallel GC | -XX:+UseParallelGC -XX:ParallelGCThreads=value | |
| Parallel Compacting GC | -XX:+UseParallelOldGC | |
| CMS GC | -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=value -XX:+UseCMSInitiatingOccupancyOnly | |
| G1 | -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC | 在JDK 6中這兩個(gè)參數(shù)必須配合使用 |
除了G1收集器外,可以通過(guò)設(shè)置上表中每種類型第一行的參數(shù)來(lái)切換GC類型,最常見的非侵入式GC就是Serial GC,它針對(duì)客戶端系統(tǒng)進(jìn)行了特別的優(yōu)化。
會(huì)影響GC性能的參數(shù)還有很多,但是上述的參數(shù)會(huì)帶來(lái)最顯著的效果,請(qǐng)切記,設(shè)置太多的參數(shù)并不一定會(huì)提升GC的性能。
GC優(yōu)化的過(guò)程
GC優(yōu)化的過(guò)程和大多數(shù)常見的提升性能的過(guò)程相似,下面是筆者使用的流程:
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)存無(wú)法省下,那么就無(wú)法進(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)來(lái)消除導(dǎo)致異常的原因。
注意:
堆轉(zhuǎn)儲(chǔ)(heap dump)是一個(gè)用來(lái)檢查Java內(nèi)存中的對(duì)象和數(shù)據(jù)的內(nèi)存文件。該文件可以通過(guò)執(zhí)行JDK中的jmap命令來(lái)創(chuàng)建。在創(chuàng)建文件的過(guò)程中,所有Java程序都將暫停,因此,不要再系統(tǒng)執(zhí)行過(guò)程中創(chuàng)建該文件。
你可以在互聯(lián)網(wǎng)上搜索heap dump的詳細(xì)說(shuō)明。對(duì)于韓國(guó)讀者,可以直接參考我去年發(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)存,然后需要通過(guò)不斷調(diào)整GC類型/內(nèi)存大小來(lá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)介紹過(guò)了jstat命令,所以在本篇文章中我將著重關(guān)注數(shù)據(jù)部分。
下面的例子展示了某個(gè)還沒(méi)有執(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ù)字,通過(guò)這個(gè)結(jié)果可以看出,我們大可不必關(guān)注新生代GC對(duì)GC性能的影響。
現(xiàn)在來(lái)看一下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ù)來(lái)生成日志。在之前的文章中筆者已經(jīng)解釋了如何分析這些日志。HPJMeter是筆者最喜歡的用于分析-verbosegc生成的日志的工具,它簡(jiǎn)單易用,使用HPJmeter可以很容易地查看GC執(zhí)行時(shí)間以及GC發(fā)生頻率。
此外,如果GC執(zhí)行時(shí)間滿足下列所有條件,就沒(méi)有必要進(jìn)行GC優(yōu)化了:
- Minor GC執(zhí)行非常迅速(50ms以內(nèi))
- Minor GC沒(méi)有頻繁執(zhí)行(大約10s執(zhí)行一次)
- Full GC執(zhí)行非常迅速(1s以內(nèi))
- Full GC沒(méi)有頻繁執(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ù)來(lái)查看具體占用了多少空間。
設(shè)置GC類型/內(nèi)存大小
設(shè)置GC類型
Oracle JVM有5種垃圾收集器,但是在JDK 7以前的版本中,你只能在Parallel GC, Parallel Compacting GC 和CMS GC之中選擇,至于具體選擇哪個(gè),則沒(méi)有具體的原則和規(guī)則。
既然這樣的話,我們?nèi)绾蝸?lái)選擇GC呢?最好的方法是把三種都用上,但是有一點(diǎn)必須明確——CMS GC通常比其他并行(Parallel)GC都要快(這是因?yàn)镃MS GC是并發(fā)的GC),如果確實(shí)如此,那只選擇CMS GC就可以了,不過(guò)CMS GC也不總是更快,當(dāng)出現(xiàn)concurrent mode failure時(shí),CMS GC就會(huì)比并行GC更慢了。
Concurrent mode failure
現(xiàn)在讓我們來(lái)深入地了解一下concurrent mode failure。
并行GC和CMS GC的最大區(qū)別是并行GC采用“標(biāo)記-整理”(Mark-Compact)算法而CMS GC采用“標(biāo)記-清除”(Mark-Sweep)算法(具體內(nèi)容可參照譯者的文章《GC算法與內(nèi)存分配策略》),compact步驟就是通過(guò)移動(dòng)內(nèi)存來(lái)消除內(nèi)存碎片,從而消除分配的內(nèi)存之間的空白區(qū)域。
對(duì)于并行GC來(lái)說(shuō),無(wú)論何時(shí)執(zhí)行Full GC,都會(huì)進(jìn)行compact工作,這消耗了太多的時(shí)間。不過(guò)在執(zhí)行完Full GC后,下次內(nèi)存分配將會(huì)變得更快(因?yàn)橹苯禹樞蚍峙湎噜彽膬?nèi)存)。
相反,CMS GC沒(méi)有compact的過(guò)程,因此CMS GC運(yùn)行的速度更快。但是也是由于沒(méi)有整理內(nèi)存,在進(jìn)行磁盤清理之前,內(nèi)存中會(huì)有很多零碎的空白區(qū)域,這也導(dǎo)致沒(méi)有足夠的空間分配給大對(duì)象。例如,在老年代還有300MB可用空間,但是連一個(gè)10MB的對(duì)象都沒(méi)有辦法被順序存儲(chǔ)在老年代中,在這種情況下,會(huì)報(bào)出“concurrent mode failure”的warning,然后系統(tǒng)執(zhí)行compact操作。但是CMS GC在這種情況下執(zhí)行的compact操作耗時(shí)要比并行GC高很多,并且這還會(huì)導(dǎo)致另一個(gè)問(wèn)題,關(guān)于“concurrent mode failure”的詳細(xì)說(shuō)明,可用參考Oracle工程師撰寫的《Understanding CMS GC Logs》。
綜上所述,你需要根據(jù)你的系統(tǒng)情況為其選擇一個(gè)最適合的GC類型。
每個(gè)系統(tǒng)都有最適合它的GC類型等著你去尋找,如果你有6臺(tái)服務(wù)器,我建議你每?jī)蓚€(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)存的大小,沒(mé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í),Full GC會(huì)耗時(shí)10-30s,具體的時(shí)間自然與對(duì)象的大小有關(guān)。
既然如此,我們?cè)撊绾卧O(shè)置內(nèi)存大小呢?通常我推薦設(shè)為500MB,這不是說(shuō)你要通過(guò)-Xms500m和-Xmx500m參數(shù)來(lái)設(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é)果。
理論上來(lái)說(shuō),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í)里是無(wú)法保證的,實(shí)際的運(yùn)行時(shí)間還依賴于服務(wù)器的性能和對(duì)象大小。因此,最好的方法是創(chuàng)建盡可能多的測(cè)量數(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)化最快地方法是什么?答案是比較性能測(cè)試的結(jié)果。為了給每臺(tái)服務(wù)器設(shè)置不同的參數(shù)并監(jiān)控它們,最好查看的是一或兩天后的數(shù)據(jù)。當(dāng)通過(guò)性能測(cè)試來(lái)進(jìn)行GC優(yōu)化時(shí),你需要在不同的測(cè)試時(shí)保證它們有相同的負(fù)載和運(yùn)行環(huán)境。然而,即使是專業(yè)的性能測(cè)試人員,想精確地控制負(fù)載也很困難,并且需要大量的時(shí)間準(zhǔn)備。因此,更加方便容易的方式是直接設(shè)置參數(shù)來(lái)運(yùn)行,然后等待運(yùn)行的結(jié)果(即使這需要消耗更多的時(shí)間)。
分析GC優(yōu)化的結(jié)果
在設(shè)置了GC參數(shù)和-verbosegc參數(shù)后,可以使用tail命令確保日志被正確地生成。如果參數(shù)設(shè)置得不正確或日志未生成,那你的時(shí)間就被白白浪費(fèi)了。如果日志收集沒(méi)有問(wèn)題的話,在收集一或兩天數(shù)據(jù)后再檢查結(jié)果。最簡(jiǎn)單的方法是把日志從服務(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í)踐了,我們將通過(guò)幾個(gè)例子來(lái)更深入地理解GC優(yōu)化。
示例1
下面這個(gè)例子是針對(duì)Service S的優(yōu)化,對(duì)于最近剛開發(fā)出來(lái)的Service S,執(zhí)行Full GC需要消耗過(guò)多的時(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í)間如下表所示:
表3:Service S的Minor GC 和Full GC的平均執(zhí)行時(shí)間| Minor GC | 54 | 2.047s | 37ms |
| Full GC | 5 | 6.946s | 1.389s |
37ms對(duì)于Minor GC來(lái)說(shuō)還不賴,但1.389s對(duì)于Full GC來(lái)說(shuō)意味著當(dāng)GC發(fā)生在數(shù)據(jù)庫(kù)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)存空間加起來(lái)有2GB,并且新生代:老年代=1:9,為了得到比使用jstat更細(xì)致的結(jié)果,還需加上-verbosegc參數(shù)獲取日志,并把三臺(tái)服務(wù)器按照如下方式設(shè)置(除此以外沒(méi)有使用任何其他參數(shù)):
- NewRatio=2
- NewRatio=3
- NewRatio=4
一天后我得到了系統(tǒng)的GC log,幸運(yùn)的是,在設(shè)置完NewRatio后系統(tǒng)沒(méi)有發(fā)生任何Full GC。
這是為什么呢?這是因?yàn)榇蟛糠謱?duì)象在創(chuàng)建后很快就被回收了,所有這些對(duì)象沒(méi)有被傳入老年代,而是在新生代就被銷毀回收了。
在這樣的情況下,就沒(méi)有必要去改變其他的參數(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í)間的長(zhǎng)短得出NewRatio=4是最佳的參數(shù)值(盡管NewRatio=4時(shí)新生代空間是最小的)。在設(shè)置完GC參數(shù)后,服務(wù)器沒(méi)有發(fā)生Full GC。
為了說(shuō)明這個(gè)問(wèn)題,下面是服務(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沒(méi)有執(zhí)行過(guò),但Minor GC被執(zhí)行了2424次。
示例2
這是一個(gè)Service A的例子。我們通過(guò)公司內(nèi)部的應(yīng)用性能管理系統(tǒng)(APM)發(fā)現(xiàn)JVM暫停了相當(dāng)長(zhǎng)的時(shí)間(超過(guò)8秒),因此我們進(jìn)行了GC優(yōu)化。我們努力尋找JVM暫停的原因,后來(lái)發(fā)現(xiàn)是因?yàn)镕ull GC執(zhí)行時(shí)間過(guò)長(zhǎng),因此我們決定進(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é)果。
之前我說(shuō)過(guò)CMS GC是最快的GC,但是上面的結(jié)果顯示在一些時(shí)候CMS耗時(shí)達(dá)到了15s。是什么導(dǎo)致了這一結(jié)果?請(qǐng)記住我之前說(shuō)的:CMS在執(zhí)行compact(整理)操作時(shí)會(huì)顯著變慢。此外,服務(wù)的內(nèi)存通過(guò)-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.890Full 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ā)生異常的具體原因,簡(jiǎn)單地說(shuō),應(yīng)該是批處理程序?qū)е铝藘?nèi)存泄漏,我們正在解決相關(guān)的問(wèn)題。
如果只對(duì)GC日志做一些短時(shí)間的分析就將相關(guān)參數(shù)部署到所有服務(wù)器上來(lái)執(zhí)行GC優(yōu)化,這將是非常危險(xiǎn)的。切記,只有當(dāng)你同時(shí)仔細(xì)分析服務(wù)的執(zhí)行情況和GC日志后,才能保證GC優(yōu)化沒(méi)有錯(cuò)誤地執(zhí)行。
在上文中,我們通過(guò)兩個(gè)GC優(yōu)化的例子來(lái)說(shuō)明了GC優(yōu)化是怎樣執(zhí)行的。正如上文中提到的,例子中設(shè)置的GC參數(shù)可以設(shè)置在相同的服務(wù)器之上,但前提是他們具有相同的CPU、操作系統(tǒng)、JDK版本并且運(yùn)行著相同的服務(wù)。此外,不要把我使用的參數(shù)照搬到你的應(yīng)用上,它們可能在你的機(jī)器上并不能起到同樣良好的效果。
總結(jié)
筆者沒(méi)有執(zhí)行heap dump并分析內(nèi)存的詳細(xì)內(nèi)容,而是通過(guò)自己的經(jīng)驗(yàn)進(jìn)行GC優(yōu)化。精確地分析內(nèi)存可以得到更好的優(yōu)化效果,不過(guò)這種分析一般只適用于內(nèi)存使用量相對(duì)固定的場(chǎng)景。如果服務(wù)嚴(yán)重過(guò)載并占有了大量的內(nèi)存,則建議你根據(jù)之前的經(jīng)驗(yàn)進(jìn)行GC優(yōu)化。
筆者已經(jīng)在一些服務(wù)上設(shè)置了G1 GC參數(shù)并進(jìn)行了性能測(cè)試,但還沒(méi)有應(yīng)用于正式的生產(chǎn)環(huán)境。G1 GC的速度快于任何其他的GC類型,但是你必須要升級(jí)到JDK 7。此外,暫時(shí)還無(wú)法保證它的穩(wěn)定性,沒(méi)有人知道運(yùn)行時(shí)是否會(huì)出現(xiàn)致命的錯(cuò)誤,因此G1
GC暫時(shí)還不適合投入應(yīng)用。
等未來(lái)JDK 7真正穩(wěn)定了(這并不是說(shuō)它現(xiàn)在不穩(wěn)定),并且WAS針對(duì)JDK 7進(jìn)行優(yōu)化后,G1 GC最終能按照預(yù)期的那樣來(lái)工作,等到那一天我們可能就不再需要GC優(yōu)化了。
想了解關(guān)于GC優(yōu)化的更多細(xì)節(jié),請(qǐng)前往Slideshare.com?查看相關(guān)資料。強(qiáng)烈推薦Everything I Ever Learned About JVM Performance Tuning @Twitter,作者是Attila Szegedi, 一名Twitter工程師,請(qǐng)花些時(shí)間好好閱讀它。
作者:純潔的微笑?
出處:www.ityouknow.com?
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。?
本文如對(duì)您有幫助,還請(qǐng)多幫?【推薦】?下此文。?
如果喜歡我的文章,請(qǐng)關(guān)注我的公眾號(hào)
本文轉(zhuǎn)自純潔的微笑博客博客園博客,原文鏈接:http://www.cnblogs.com/ityouknow/p/7653129.html,如需轉(zhuǎn)載請(qǐng)自行聯(lián)系原作者
總結(jié)
以上是生活随笔為你收集整理的jvm系列(十):如何优化Java GC「译」的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【1】ASP.NET异步(1)
- 下一篇: 《Redis实战》一第一部分 入门