本文由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》,本文的作者是韓國人,寫在JDK 1.8發(fā)布之前尤辱,雖然有些地方有些許過時,但整體內(nèi)容還是非常有價值的厢岂。譯者此前也看到有人翻譯了本文光督,發(fā)現(xiàn)其中有許多錯漏生硬和語焉不詳之處,因此決定自己翻譯一份咪笑,供大家分享可帽。
本文是“成為Java GC專家”系列文章的第三篇娄涩,在系列的第一篇文章《理解Java GC》中窗怒,我們了解到了不同GC算法的執(zhí)行過程、GC的工作原理蓄拣、新生代和老年代的概念扬虚、JDK 7中你需要了解的5種GC類型以及每一種GC對性能的影響。
在系列的第二篇文章《如何監(jiān)控Java GC》中筆者已經(jīng)解釋了JVM進行實時GC的原理球恤、監(jiān)控GC的方法以及可以使這一過程更加迅速高效的工具辜昵。
在第三篇文章中,筆者將基于實際生產(chǎn)環(huán)境中的案例咽斧,介紹幾個GC優(yōu)化的最佳參數(shù)設(shè)置堪置。在此我們假設(shè)你已經(jīng)理解了本系列前兩篇文章的內(nèi)容躬存,因此為了更深入的理解本文所講內(nèi)容,我建議你在閱讀本篇文章之前先仔細閱讀這兩篇文章舀锨。
GC優(yōu)化是必要的嗎岭洲?
或者更準(zhǔn)確地說,GC優(yōu)化對Java基礎(chǔ)服務(wù)來說是必要的嗎坎匿?答案是否定的盾剩,事實上GC優(yōu)化對Java基礎(chǔ)服務(wù)來說在有些場合是可以省去的,但前提是這些正在運行的Java系統(tǒng)替蔬,必須包含以下參數(shù)或行為:
- 內(nèi)存大小已經(jīng)通過-Xms和-Xmx參數(shù)指定過
- 運行在server模式下(使用-server參數(shù))
- 系統(tǒng)中沒有殘留超時日志之類的錯誤日志
換句話說告私,如果你在運行時沒有手動設(shè)置內(nèi)存大小并且打印出了過多的超時日志,那你就需要對系統(tǒng)進行GC優(yōu)化承桥。
不過你需要時刻謹(jǐn)記一句話:GC tuning is the last task to be done.
現(xiàn)在來想一想GC優(yōu)化的最根本原因驻粟,垃圾收集器的工作就是清除Java創(chuàng)建的對象,垃圾收集器需要清理的對象數(shù)量以及要執(zhí)行的GC數(shù)量均取決于已創(chuàng)建的對象數(shù)量凶异。因此格嗅,為了使你的系統(tǒng)在GC上表現(xiàn)良好,首先需要減少創(chuàng)建對象的數(shù)量唠帝。
俗話說“冰凍三尺非一日之寒”屯掖,我們在編碼時要首先要把下面這些小細節(jié)做好,否則一些瑣碎的不良代碼累積起來將讓GC的工作變得繁重而難于管理:
- 使用
StringBuilder
或StringBuffer
來代替String
- 盡量少輸出日志
盡管如此襟衰,仍然會有我們束手無策的情況贴铜。XML和JSON解析過程往往占用了最多的內(nèi)存,即使我們已經(jīng)盡可能地少用String瀑晒、少輸出日志绍坝,仍然會有大量的臨時內(nèi)存(大約10-100MB)被用來解析XML或JSON文件,但我們又很難棄用XML和JSON苔悦。在此轩褐,你只需要知道這一過程會占據(jù)大量內(nèi)存即可。
如果在經(jīng)過幾次重復(fù)的優(yōu)化后應(yīng)用程序的內(nèi)存用量情況有所改善玖详,那么久可以啟動GC優(yōu)化了把介。
筆者總結(jié)了GC優(yōu)化的兩個目的:
- 將進入老年代的對象數(shù)量降到最低
- 減少Full GC的執(zhí)行時間
將進入老年代的對象數(shù)量降到最低
除了可以在JDK 7及更高版本中使用的G1收集器以外,其他分代GC都是由Oracle JVM提供的蟋座。關(guān)于分代GC拗踢,就是對象在Eden區(qū)被創(chuàng)建,隨后被轉(zhuǎn)移到Survivor區(qū)向臀,在此之后剩余的對象會被轉(zhuǎn)入老年代巢墅。也有一些對象由于占用內(nèi)存過大,在Eden區(qū)被創(chuàng)建后會直接被傳入老年代。老年代GC相對來說會比新生代GC更耗時君纫,因此驯遇,減少進入老年代的對象數(shù)量可以顯著降低Full GC的頻率。你可能會以為減少進入老年代的對象數(shù)量意味著把它們留在新生代蓄髓,事實正好相反妹懒,新生代內(nèi)存的大小是可以調(diào)節(jié)的。
降低Full GC的時間
Full GC的執(zhí)行時間比Minor GC要長很多双吆,因此眨唬,如果在Full GC上花費過多的時間(超過1s),將可能出現(xiàn)超時錯誤好乐。
- 如果通過減小老年代內(nèi)存來減少Full GC時間匾竿,可能會引起
OutOfMemoryError
或者導(dǎo)致Full GC的頻率升高。 - 另外蔚万,如果通過增加老年代內(nèi)存來降低Full GC的頻率岭妖,F(xiàn)ull GC的時間可能因此增加。
因此反璃,你需要把老年代的大小設(shè)置成一個“合適”的值昵慌。
影響GC性能的參數(shù)
正如我在系列的第一篇文章《理解Java GC》末尾提到的,不要幻想著“如果有人用他設(shè)置的GC參數(shù)獲取了不錯的性能淮蜈,我們?yōu)槭裁床粡?fù)制他的參數(shù)設(shè)置呢斋攀?”,因為對于不用的Web服務(wù)梧田,它們創(chuàng)建的對象大小和生命周期都不相同淳蔼。
舉一個簡單的例子,如果一個任務(wù)的執(zhí)行條件是A裁眯,B鹉梨,C,D和E穿稳,另一個完全相同的任務(wù)執(zhí)行條件只有A和B存皂,那么哪一個任務(wù)執(zhí)行速度更快呢?作為常識來講逢艘,答案很明顯是后者旦袋。
Java GC參數(shù)的設(shè)置也是這個道理,設(shè)置好幾個參數(shù)并不會提升GC執(zhí)行的速度埋虹,反而會使它變得更慢猜憎。GC優(yōu)化的基本原則是將不同的GC參數(shù)應(yīng)用到兩個及以上的服務(wù)器上然后比較它們的性能娩怎,然后將那些被證明可以提高性能或減少GC執(zhí)行時間的參數(shù)應(yīng)用于最終的工作服務(wù)器上搔课。
下面這張表展示了與內(nèi)存大小相關(guān)且會影響GC性能的GC參數(shù)
表1:GC優(yōu)化需要考慮的JVM參數(shù)
類型 | 參數(shù) | 描述 |
---|---|---|
堆內(nèi)存大小 | -Xms |
啟動JVM時堆內(nèi)存的大小 |
-Xmx |
堆內(nèi)存最大限制 | |
新生代空間大小 | -XX:NewRatio |
新生代和老年代的內(nèi)存比 |
-XX:NewSize |
新生代內(nèi)存大小 | |
-XX:SurvivorRatio |
Eden區(qū)和Survivor區(qū)的內(nèi)存比 |
筆者在進行GC優(yōu)化時最常用的參數(shù)是-Xms
,-Xmx
和-XX:NewRatio
。-Xms
和-Xmx
參數(shù)通常是必須的爬泥,所以NewRatio
的值將對GC性能產(chǎn)生重要的影響柬讨。
有些人可能會問如何設(shè)置永久代內(nèi)存大小,你可以用-XX:PermSize
和-XX:MaxPermSize
參數(shù)來進行設(shè)置袍啡,但是要記住踩官,只有當(dāng)出現(xiàn)OutOfMemoryError
錯誤時你才需要去設(shè)置永久代內(nèi)存。
還有一個會影響GC性能的因素是垃圾收集器的類型境输,下表展示了關(guān)于GC類型的可選參數(shù)(基于JDK 6.0):
表2:GC類型可選參數(shù)
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中這兩個參數(shù)必須配合使用 |
除了G1收集器外蔗牡,可以通過設(shè)置上表中每種類型第一行的參數(shù)來切換GC類型,最常見的非侵入式GC就是Serial GC嗅剖,它針對客戶端系統(tǒng)進行了特別的優(yōu)化辩越。
會影響GC性能的參數(shù)還有很多,但是上述的參數(shù)會帶來最顯著的效果信粮,請切記黔攒,設(shè)置太多的參數(shù)并不一定會提升GC的性能。
GC優(yōu)化的過程
GC優(yōu)化的過程和大多數(shù)常見的提升性能的過程相似强缘,下面是筆者使用的流程:
1. 監(jiān)控GC狀態(tài)
你需要監(jiān)控GC從而檢查系統(tǒng)中運行的GC的各種狀態(tài)督惰,具體方法請查看系列的第二篇文章《如何監(jiān)控Java GC》。
2. 分析監(jiān)控結(jié)果后決定是否需要優(yōu)化GC
在檢查GC狀態(tài)后旅掂,你需要分析監(jiān)控結(jié)構(gòu)并決定是否需要進行GC優(yōu)化赏胚。如果分析結(jié)果顯示運行GC的時間只有0.1-0.3秒,那么就不需要把時間浪費在GC優(yōu)化上商虐,但如果運行GC的時間達到1-3秒栅哀,甚至大于10秒,那么GC優(yōu)化將是很有必要的称龙。
但是留拾,如果你已經(jīng)分配了大約10GB內(nèi)存給Java,并且這些內(nèi)存無法省下鲫尊,那么就無法進行GC優(yōu)化了痴柔。在進行GC優(yōu)化之前,你需要考慮為什么你需要分配這么大的內(nèi)存空間疫向,如果你分配了1GB或2GB大小的內(nèi)存并且出現(xiàn)了OutOfMemoryError
咳蔚,那你就應(yīng)該執(zhí)行堆轉(zhuǎn)儲(heap dump)來消除導(dǎo)致異常的原因。
注意:
堆轉(zhuǎn)儲(heap dump)是一個用來檢查Java內(nèi)存中的對象和數(shù)據(jù)的內(nèi)存文件搔驼。該文件可以通過執(zhí)行JDK中的jmap
命令來創(chuàng)建谈火。在創(chuàng)建文件的過程中,所有Java程序都將暫停舌涨,因此糯耍,不要在系統(tǒng)執(zhí)行過程中創(chuàng)建該文件。
你可以在互聯(lián)網(wǎng)上搜索heap dump的詳細說明。對于韓國讀者温技,可以直接參考我去年發(fā)布的書:《The story of troubleshooting for Java developers and system operators》 (Sangmin Lee, Hanbit Media, 2011, 416 pages)
3. 設(shè)置GC類型/內(nèi)存大小
如果你決定要進行GC優(yōu)化革为,那么你需要選擇一個GC類型并且為它設(shè)置內(nèi)存大小。此時如果你有多個服務(wù)器舵鳞,請如上文提到的那樣震檩,在每臺機器上設(shè)置不同的GC參數(shù)并分析它們的區(qū)別。
4. 分析結(jié)果
在設(shè)置完GC參數(shù)后就可以開始收集數(shù)據(jù)蜓堕,請在收集至少24小時后再進行結(jié)果分析抛虏。如果你足夠幸運,你可能會找到系統(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é)中,你將會看到上述每一步所做的具體工作崎逃。
監(jiān)控GC狀態(tài)并分析結(jié)果
在運行中的Web應(yīng)用服務(wù)器(Web Application Server, WAS)上查看GC狀態(tài)的最佳方式就是使用jstat
命令掷倔。筆者在《如何監(jiān)控Java GC》中已經(jīng)介紹過了jstat
命令,所以在本篇文章中我將著重關(guān)注數(shù)據(jù)部分个绍。
下面的例子展示了某個還沒有執(zhí)行GC優(yōu)化的JVM的狀態(tài)(雖然它并不是運行服務(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)用程序啟動到采樣時發(fā)生 Young GC 的次數(shù))和YGCT(從應(yīng)用程序啟動到采樣時 Young GC 所用的時間(秒)),計算YGCT/YGC會得出巴柿,平均每次新生代的GC耗時50ms凛虽,這是一個很小的數(shù)字,通過這個結(jié)果可以看出广恢,我們大可不必關(guān)注新生代GC對GC性能的影響凯旋。
現(xiàn)在來看一下FGC( 從應(yīng)用程序啟動到采樣時發(fā)生 Full GC 的次數(shù))和FGCT(從應(yīng)用程序啟動到采樣時 Full GC 所用的時間(秒)),計算FGCT/FGC會得出钉迷,平均每次老年代的GC耗時19.68s至非。有可能是執(zhí)行了三次Full GC,每次耗時19.68s糠聪,也有可能是有兩次只花了1s,另一次花了58s荒椭。不管是哪一種情況,GC優(yōu)化都是很有必要的舰蟆。
使用jstat
命令可以很容易地查看GC狀態(tài)趣惠,但是分析GC的最佳方式是加上-verbosegc
參數(shù)來生成日志狸棍。在之前的文章中筆者已經(jīng)解釋了如何分析這些日志。HPJMeter是筆者最喜歡的用于分析-verbosegc
生成的日志的工具信卡,它簡單易用隔缀,使用HPJmeter可以很容易地查看GC執(zhí)行時間以及GC發(fā)生頻率题造。
此外傍菇,如果GC執(zhí)行時間滿足下列所有條件,就沒有必要進行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í)行一次)
括號中的數(shù)字并不是絕對的界赔,它們也隨著服務(wù)的狀態(tài)而變化丢习。有些服務(wù)可能要求一次Full GC在0.9s以內(nèi),而有些則會放得更寬一些淮悼。因此咐低,對于不同的服務(wù),需要按照不同的標(biāo)準(zhǔn)考慮是否需要執(zhí)行GC優(yōu)化袜腥。
當(dāng)檢查GC狀態(tài)時见擦,不能只查看Minor GC和Full GC的時間,還必須要關(guān)注GC執(zhí)行的次數(shù)羹令。如果新生代空間太小鲤屡,Minor GC將會非常頻繁地執(zhí)行(有時每秒會執(zhí)行一次,甚至更多)福侈。此外酒来,傳入老年代的對象數(shù)目會上升,從而導(dǎo)致Full GC的頻率升高肪凛。因此堰汉,在執(zhí)行jstat
命令時,請使用-gccapacity
參數(shù)來查看具體占用了多少空間伟墙。
設(shè)置GC類型/內(nèi)存大小
設(shè)置GC類型
Oracle JVM有5種垃圾收集器翘鸭,但是在JDK 7以前的版本中,你只能在Parallel GC戳葵,Parallel Compacting GC 和CMS GC之中選擇矮固,至于具體選擇哪個,則沒有具體的原則和規(guī)則譬淳。
既然這樣的話档址,我們?nèi)绾蝸磉x擇GC呢?最好的方法是把三種都用上邻梆,但是有一點必須明確——CMS GC通常比其他并行(Parallel)GC都要快(這是因為CMS GC是并發(fā)的GC)守伸,如果確實如此,那只選擇CMS GC就可以了浦妄,不過CMS GC也不總是更快尼摹,當(dāng)出現(xiàn)concurrent mode failure時见芹,CMS GC就會比并行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步驟就是通過移動內(nèi)存來消除內(nèi)存碎片,從而消除分配的內(nèi)存之間的空白區(qū)域和二。
對于并行GC來說徘铝,無論何時執(zhí)行Full GC,都會進行compact工作惯吕,這消耗了太多的時間惕它。不過在執(zhí)行完Full GC后,下次內(nèi)存分配將會變得更快(因為直接順序分配相鄰的內(nèi)存)废登。
相反淹魄,CMS GC沒有compact的過程,因此CMS GC運行的速度更快堡距。但是也是由于沒有整理內(nèi)存甲锡,在進行磁盤清理之前,內(nèi)存中會有很多零碎的空白區(qū)域羽戒,這也導(dǎo)致沒有足夠的空間分配給大對象缤沦。例如,在老年代還有300MB可用空間半醉,但是連一個10MB的對象都沒有辦法被順序存儲在老年代中疚俱,在這種情況下,會報出“concurrent mode failure”的warning缩多,然后系統(tǒng)執(zhí)行compact操作呆奕。但是CMS GC在這種情況下執(zhí)行的compact操作耗時要比并行GC高很多母谎,并且這還會導(dǎo)致另一個問題贡必,關(guān)于“concurrent mode failure”的詳細說明,可用參考Oracle工程師撰寫的《Understanding CMS GC Logs》祭往。
綜上所述逊抡,你需要根據(jù)你的系統(tǒng)情況為其選擇一個最適合的GC類型姆泻。
每個系統(tǒng)都有最適合它的GC類型等著你去尋找,如果你有6臺服務(wù)器冒嫡,我建議你每兩個服務(wù)器設(shè)置相同的參數(shù)拇勃,然后加上-verbosegc
參數(shù)再分析結(jié)果。
設(shè)置內(nèi)存大小
下面展示了內(nèi)存大小孝凌、GC運行次數(shù)和GC運行時間之間的關(guān)系:
大內(nèi)存空間
- 減少了GC的次數(shù)
- 提高了GC的運行時間
小內(nèi)存空間
- 增多了GC的次數(shù)
- 降低了GC的運行時間
關(guān)于如何設(shè)置內(nèi)存的大小方咆,沒有一個標(biāo)準(zhǔn)答案,如果服務(wù)器資源充足并且Full GC能在1s內(nèi)完成蟀架,把內(nèi)存設(shè)為10GB也是可以的瓣赂,但是大部分服務(wù)器并不處在這種狀態(tài)中榆骚,當(dāng)內(nèi)存設(shè)為10GB時,F(xiàn)ull GC會耗時10-30s煌集,具體的時間自然與對象的大小有關(guān)妓肢。
既然如此,我們該如何設(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是一個不錯的選擇(300MB(默認程序占用)+ 500MB(老年代最小空間)+200MB(空閑內(nèi)存))色徘。這意味著你需要為老年代設(shè)置至少500MB空間恭金,因此如果你有三個運行服務(wù)器,可以把它們的內(nèi)存分別設(shè)置為1GB褂策,1.5GB横腿,2GB,然后檢查結(jié)果斤寂。
理論上來說耿焊,GC執(zhí)行速度應(yīng)該遵循1GB > 1.5GB > 2GB,1GB內(nèi)存時GC執(zhí)行速度最快遍搞。然而罗侯,理論上的1GB內(nèi)存Full GC消耗1s、2GB內(nèi)存Full GC消耗2s在現(xiàn)實里是無法保證的溪猿,實際的運行時間還依賴于服務(wù)器的性能和對象大小钩杰。因此,最好的方法是創(chuàng)建盡可能多的測量數(shù)據(jù)并監(jiān)控它們诊县。
在設(shè)置內(nèi)存空間大小時讲弄,你還需要設(shè)置一個參數(shù):NewRatio
。NewRatio
的值是新生代和老年代空間大小的比例依痊。如果XX:NewRatio=1
避除,則新生代空間:老年代空間=1:1,如果堆內(nèi)存為1GB胸嘁,則新生代:老年代=500MB:500MB瓶摆。如果NewRatio
等于2,則新生代:老年代=1:2性宏,因此群井,NewRatio
的值設(shè)置得越大,則老年代空間越大衔沼,新生代空間越小蝌借。
你可能會認為把NewRatio
設(shè)為1會是最好的選擇昔瞧,然而事實并非如此,根據(jù)筆者的經(jīng)驗菩佑,當(dāng)NewRatio
設(shè)為2或3時自晰,整個GC的狀態(tài)表現(xiàn)得更好。
完成GC優(yōu)化最快地方法是什么稍坯?答案是比較性能測試的結(jié)果酬荞。為了給每臺服務(wù)器設(shè)置不同的參數(shù)并監(jiān)控它們,最好查看的是一或兩天后的數(shù)據(jù)瞧哟。當(dāng)通過性能測試來進行GC優(yōu)化時混巧,你需要在不同的測試時保證它們有相同的負載和運行環(huán)境。然而勤揩,即使是專業(yè)的性能測試人員咧党,想精確地控制負載也很困難,并且需要大量的時間準(zhǔn)備陨亡。因此傍衡,更加方便容易的方式是直接設(shè)置參數(shù)來運行,然后等待運行的結(jié)果(即使這需要消耗更多的時間)负蠕。
分析GC優(yōu)化的結(jié)果
在設(shè)置了GC參數(shù)和-verbosegc
參數(shù)后蛙埂,可以使用tail命令確保日志被正確地生成。如果參數(shù)設(shè)置得不正確或日志未生成遮糖,那你的時間就被白白浪費了绣的。如果日志收集沒有問題的話,在收集一或兩天數(shù)據(jù)后再檢查結(jié)果欲账。最簡單的方法是把日志從服務(wù)器移到你的本地PC上屡江,然后用HPJMeter分析數(shù)據(jù)。
在分析結(jié)果時敬惦,請關(guān)注下列幾點(這個優(yōu)先級是筆者根據(jù)自己的經(jīng)驗擬定的盼理,我認為選取GC參數(shù)時應(yīng)考慮的最重要的因素是Full GC的運行時間。):
- 單次Full GC運行時間
- 單次Minor GC運行時間
- Full GC運行間隔
- Minor GC運行間隔
- 整個Full GC的時間
- 整個Minor GC的運行時間
- 整個GC的運行時間
- Full GC的執(zhí)行次數(shù)
- Minor GC的執(zhí)行次數(shù)
找到最佳的GC參數(shù)是件非常幸運的俄删,然而在大多數(shù)時候宏怔,我們并不會如此幸運,在進行GC優(yōu)化時一定要小心謹(jǐn)慎畴椰,因為當(dāng)你試圖一次完成所有的優(yōu)化工作時臊诊,可能會出現(xiàn)OutOfMemoryError
錯誤。
優(yōu)化案例
到目前為止斜脂,我們一直在從理論上介紹GC優(yōu)化抓艳,現(xiàn)在是時候?qū)⑦@些理論付諸實踐了,我們將通過幾個例子來更深入地理解GC優(yōu)化帚戳。
示例1
下面這個例子是針對Service S的優(yōu)化玷或,對于最近剛開發(fā)出來的Service S儡首,執(zhí)行Full GC需要消耗過多的時間。
現(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ū)的值對于最初的GC優(yōu)化并不重要偏友,而YGC參數(shù)的值更加對于這次優(yōu)化更為重要蔬胯。
平均執(zhí)行一次Minor GC和Full GC消耗的時間如下表所示:
表3:Service S的Minor GC 和Full GC的平均執(zhí)行時間
GC類型 | GC執(zhí)行次數(shù) | GC執(zhí)行時間 | 平均值 |
---|---|---|---|
Minor GC | 54 | 2.047s | 37ms |
Full GC | 5 | 6.946s | 1.389s |
37ms對于Minor GC來說還不賴,但1.389s對于Full GC來說意味著當(dāng)GC發(fā)生在數(shù)據(jù)庫Timeout設(shè)置為1s的系統(tǒng)中時位他,可能會頻繁出現(xiàn)超時現(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
更細致的結(jié)果靡菇,還需加上-verbosegc
參數(shù)獲取日志重归,并把三臺服務(wù)器按照如下方式設(shè)置(除此以外沒有使用任何其他參數(shù)):
- NewRatio=2
- NewRatio=3
- NewRatio=4
一天后我得到了系統(tǒng)的GC log米愿,幸運的是厦凤,在設(shè)置完NewRatio后系統(tǒng)沒有發(fā)生任何Full GC。
這是為什么呢育苟?這是因為大部分對象在創(chuàng)建后很快就被回收了较鼓,所有這些對象沒有被傳入老年代,而是在新生代就被銷毀回收了违柏。
在這樣的情況下博烂,就沒有必要去改變其他的參數(shù)值了,只要選擇一個最合適的NewRatio
值即可漱竖。那么禽篱,如何確定最佳的NewRatio值呢?為此馍惹,我們分析一下每種NewRatio
值下Minor GC的平均響應(yīng)時間躺率。
在每種參數(shù)下Minor GC的平均響應(yīng)時間如下:
- NewRatio=2:45ms
- NewRatio=3:34ms
- NewRatio=4:30ms
我們可以根據(jù)GC時間的長短得出NewRatio=4是最佳的參數(shù)值(盡管NewRatio=4時新生代空間是最小的)。在設(shè)置完GC參數(shù)后万矾,服務(wù)器沒有發(fā)生Full GC悼吱。
為了說明這個問題,下面是服務(wù)執(zhí)行一段時間后執(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
你可能會認為是服務(wù)器接收的請求少才使得GC發(fā)生的頻率較低良狈,實際上后添,雖然Full GC沒有執(zhí)行過,但Minor GC被執(zhí)行了2424次薪丁。
示例2
這是一個Service A的例子遇西。我們通過公司內(nèi)部的應(yīng)用性能管理系統(tǒng)(APM)發(fā)現(xiàn)JVM暫停了相當(dāng)長的時間(超過8秒)馅精,因此我們進行了GC優(yōu)化。我們努力尋找JVM暫停的原因粱檀,后來發(fā)現(xiàn)是因為Full GC執(zhí)行時間過長硫嘶,因此我們決定進行GC優(yōu)化。
在GC優(yōu)化的開始階段梧税,我們加上了-verbosegc
參數(shù)沦疾,結(jié)果如下圖所示:
上圖是由HPJMeter生成的圖片之一。橫坐標(biāo)表示JVM執(zhí)行的時間第队,縱坐標(biāo)表示每次GC的時間哮塞。CMS為綠點,表示Full GC的結(jié)果凳谦,而Parallel Scavenge為藍點忆畅,表示Minor GC的結(jié)果。
之前我說過CMS GC是最快的GC尸执,但是上面的結(jié)果顯示在一些時候CMS耗時達到了15s家凯。是什么導(dǎo)致了這一結(jié)果?請記住我之前說的:CMS在執(zhí)行compact(整理)操作時會顯著變慢如失。此外绊诲,服務(wù)的內(nèi)存通過-Xms1g
和=Xmx4g
設(shè)置了,而分配的內(nèi)存只有4GB褪贵。
因此筆者將GC類型從CMS GC改為了Parallel GC掂之,把內(nèi)存大小設(shè)為2GB,并把NewRatio
設(shè)為3脆丁。在執(zhí)行jstat -gcutil
幾小時后的結(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的時間縮短了世舰,變成了每次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)存空間越小震蒋,運行結(jié)果最少。下圖展示了性能最好的Case 6的結(jié)果圖丐怯,它的最慢響應(yīng)時間只有1.7s喷好,并且響應(yīng)時間的平均值已經(jīng)被控制到了1s以內(nèi)。
基于上圖的結(jié)果读跷,按照Case 6調(diào)整了GC參數(shù)梗搅,但這卻導(dǎo)致每晚都會發(fā)生OutOfMemoryError
。很難解釋發(fā)生異常的具體原因,簡單地說无切,應(yīng)該是批處理程序?qū)е铝藘?nèi)存泄漏荡短,我們正在解決相關(guān)的問題。
如果只對GC日志做一些短時間的分析就將相關(guān)參數(shù)部署到所有服務(wù)器上來執(zhí)行GC優(yōu)化哆键,這將是非常危險的掘托。切記,只有當(dāng)你同時仔細分析服務(wù)的執(zhí)行情況和GC日志后籍嘹,才能保證GC優(yōu)化沒有錯誤地執(zhí)行闪盔。
在上文中,我們通過兩個GC優(yōu)化的例子來說明了GC優(yōu)化是怎樣執(zhí)行的辱士。正如上文中提到的泪掀,例子中設(shè)置的GC參數(shù)可以設(shè)置在相同的服務(wù)器之上,但前提是他們具有相同的CPU颂碘、操作系統(tǒng)异赫、JDK版本并且運行著相同的服務(wù)。此外头岔,不要把我使用的參數(shù)照搬到你的應(yīng)用上塔拳,它們可能在你的機器上并不能起到同樣良好的效果。
總結(jié)
筆者沒有執(zhí)行heap dump并分析內(nèi)存的詳細內(nèi)容峡竣,而是通過自己的經(jīng)驗進行GC優(yōu)化靠抑。精確地分析內(nèi)存可以得到更好的優(yōu)化效果,不過這種分析一般只適用于內(nèi)存使用量相對固定的場景澎胡。如果服務(wù)嚴(yán)重過載并占有了大量的內(nèi)存孕荠,則建議你根據(jù)之前的經(jīng)驗進行GC優(yōu)化。
筆者已經(jīng)在一些服務(wù)上設(shè)置了G1 GC參數(shù)并進行了性能測試攻谁,但還沒有應(yīng)用于正式的生產(chǎn)環(huán)境。G1 GC的速度快于任何其他的GC類型弯予,但是你必須要升級到JDK 7戚宦。此外,暫時還無法保證它的穩(wěn)定性锈嫩,沒有人知道運行時是否會出現(xiàn)致命的錯誤受楼,因此G1 GC暫時還不適合投入應(yīng)用。
等未來JDK 7真正穩(wěn)定了(這并不是說它現(xiàn)在不穩(wěn)定)呼寸,并且WAS針對JDK 7進行優(yōu)化后艳汽,G1 GC最終能按照預(yù)期的那樣來工作,等到那一天我們可能就不再需要GC優(yōu)化了对雪。
想了解關(guān)于GC優(yōu)化的更多細節(jié)河狐,請前往Slideshare.com 查看相關(guān)資料。強烈推薦Everything I Ever Learned About JVM Performance Tuning @Twitter,作者是Attila Szegedi, 一名Twitter工程師,請花些時間好好閱讀它馋艺。