【JVM性能優(yōu)化】面向CMS垃圾回收器的性能優(yōu)化方案

前提概要
如果沒有冬天,春天不會如此悅?cè)耍蝗绻麤]有偶爾的不幸,幸運不會如此受人歡迎。
CMS垃圾回收的6個重要階段
initial-mark 初始標(biāo)記(CMS的第一個STW階段),標(biāo)記GC Root直接引用的對象,GC Root直接引用的對象不多,所以很快。
concurrent-mark并發(fā)標(biāo)記階段,由第一階段標(biāo)記過的對象出發(fā),所有可達的對象都在本階段標(biāo)記。
concurrent-preclean 并發(fā)預(yù)清理階段,也是一個并發(fā)執(zhí)行的階段。在本階段,會查找前一階段執(zhí)行過程中,[從新生代晉升或新分配或被更新的對象]。通過并發(fā)地重新掃描這些對象,預(yù)清理階段可以減少下一個stop-the-world 重新標(biāo)記階段的工作量。
concurrent-abortable-preclean,并發(fā)可中止的預(yù)清理階段。這個階段其實跟上一個階段做的東西一樣,也是為了減少下一個STW重新標(biāo)記階段的工作量。增加這一階段是為了讓我們可以控制這個階段的結(jié)束時機,比如掃描多長時間(默認(rèn)5秒)或者Eden區(qū)使用占比達到期望比例(默認(rèn)50%)就結(jié)束本階段。
remark重標(biāo)記階段(CMS的第二個STW階段),暫停所有用戶線程,從GC Root開始重新掃描整堆,標(biāo)記存活的對象。需要注意的是,雖然CMS只回收老年代的垃圾對象,但是這個階段依然需要掃描新生代,因為很多GC Root都在新生代,而這些GC Root指向的對象又在老年代,這稱為“跨代引用”。
concurrent-sweep ,并發(fā)清理。
分析
分析其GC日志,發(fā)現(xiàn)GC發(fā)生在CMS的收集階段。

箭頭1 顯示abortable-preclean階段耗時4.04秒。
箭頭2 顯示的是remark階段,耗時0.11秒。
雖然abortable-preclean階段是concurrent的,不會暫停其他的用戶線程。就算不優(yōu)化,可能影響也不大。
調(diào)優(yōu)之前先看下該應(yīng)用的GC統(tǒng)計數(shù)據(jù),包括GC次數(shù),耗時:

統(tǒng)計期間內(nèi)(18天)發(fā)生CMS GC 69次,其中abortable preclean階段平均耗時2.45秒,final remark階段平均112ms,最大耗時170ms。
優(yōu)化目標(biāo)
降低abortable preclean時間,而且不增加final remark的時間(因為remark是STW的)。
JVM參數(shù)調(diào)優(yōu)
第一次調(diào)優(yōu)
先嘗試調(diào)低abortable preclean階段的時間,看看效果。
有兩個參數(shù)可以控制這個階段何時結(jié)束:
-XX:CMSMaxAbortablePrecleanTime=5000
復(fù)制代碼默認(rèn)值5s,代表該階段最大的持續(xù)時間
-XX:CMSScheduleRemarkEdenPenetration=50
復(fù)制代碼默認(rèn)值50%,代表Eden區(qū)使用比例超過50%就結(jié)束該階段進入remark
調(diào)整為最大持續(xù)時間為1s,Eden區(qū)使用占比10%,如下:
-XX:CMSMaxAbortablePrecleanTime=1000
-XX:CMSScheduleRemarkEdenPenetration=10
復(fù)制代碼為什么調(diào)整成這樣兩個值:首先每次CMS都發(fā)生在老年代使用占比達到80%時,因為這是由下面兩個參數(shù)決定的:
-XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly
復(fù)制代碼這兩個設(shè)置一般配合使用,一般用于『降低CMS GC頻率或者增加頻率、減少GC時長』的需求
-XX:CMSInitiatingOccupancyFraction=80 是指設(shè)定CMS在對內(nèi)存占用率達到80%的時候開始GC(因為CMS會有浮動垃圾,所以一般都較早啟動GC);
-XX:+UseCMSInitiatingOccupancyOnly :標(biāo)志來命令JVM不基于運行時收集的數(shù)據(jù)來啟動CMS垃圾收集周期。
當(dāng)該標(biāo)志被開啟時,JVM通過CMSInitiatingOccupancyFraction的值進行每一次CMS收集,而不僅僅是第一次。(否則后續(xù)會動態(tài)控制回收閾值)
(慎用) 因此,只有當(dāng)我們充足的理由(比如測試)并且對應(yīng)用程序產(chǎn)生的對象的生命周期有深刻的認(rèn)知時,才應(yīng)該使用該標(biāo)志。
老年代的增長是由于部分對象在
Minor GC后仍然存活,被晉升到老年代,導(dǎo)致老年代使用占比增長的,也就是在每次CMS GC發(fā)生之前剛剛發(fā)生過一次Minor GC,所以在那一刻新生代的使用占比是很低的。
那么我們預(yù)計這個時候盡快結(jié)束abortable preclean階段,在remark時就不需要掃描太多的Eden區(qū)對象,remark STW的時間也就不會太長。
第一次調(diào)整參數(shù)

在統(tǒng)計期間(17小時左右)內(nèi),發(fā)生過2次CMS GC。Abortable Preclean 平均耗時835ms,這是預(yù)期內(nèi)的。但是Final Remark 平均耗時495ms(調(diào)整前是112ms),其中一次是80ms,另一次是910ms!將近1秒鐘!Remark是STW的!對于要求低延時的應(yīng)用來說這是無法接受的!

[YG occupancy: 181274 K (1887488 K)] - 年輕代當(dāng)前占用情況和總?cè)萘?/p>
耗時80ms的這次remark發(fā)生時(早上9點,非高峰時段),新生代(YG)占用181.274M。
remark耗時910ms的那次GC日志
[YG occupancy: 773427 K (1887488 K)]
耗時910ms的這次remark發(fā)生時(晚上10點左右,高峰時段),新生代(YG)占用773.427M。因為這個時候高峰期,新生代的占用量上升的非常快,幾乎同樣的時間內(nèi),非高峰時段僅上升到181M,但是高峰時段就上升到773M。
如果
abortale preclean階段時間太短,隨后在remark時,新生代占用越大,則remark持續(xù)的時間(STW)越長。不縮短abortale preclean耗時會出現(xiàn)過程gc;縮短的話,remark階段又會變長,而且是STW,更不能接受。
對于這種情況,CMS提供了CMSScavengeBeforeRemark參數(shù),嘗試在remark階段之前進行一次Minor GC,以降低新生代的占用。
第二次調(diào)優(yōu)
增加 -XX:+CMSScavengeBeforeRemark 不是沒有代價的,因為這會增加一次Minor GC停頓。所以這個方案好或者不好的判斷標(biāo)準(zhǔn)就是:增加CMSScavengeBeforeRemark參數(shù)之后的minor GC停頓時間 + remark 停頓時間如果比增加之前的remark GC停頓時間要小,這才是好的方案。
-XX:+CMSScavengeBeforeRemark:在CMS GC前啟動一次ygc,目的在于減少old gen對ygc gen的引用,降低remark時的開銷-----一般CMS的GC耗時 80%都在remark階段
第二次調(diào)整的結(jié)果
在統(tǒng)計期間(20小時左右)內(nèi),發(fā)生3次CMS GC。Abortable preclean 平均耗時693ms。Final remark平均耗時50ms,最大耗時60ms。Final remark的時間比調(diào)優(yōu)前的平均時間(112ms)更低。
3次CMS GC remark前的Minor GC日志分析
第1次是非高峰時段的表現(xiàn),Minor GC 耗時 0.01s + remark耗時 0.06s = 0.07s = 70ms,如下
第2次是高峰時段,Minor GC 耗時 0.01s + remark耗時 0.05s = 0.06s = 60ms,如下
第3次是非高峰時段,Minor GC 耗時 0.00s + remark耗時 0.04s = 0.04s = 40ms,如下
所以,3次Minor GC + remark耗時的平均耗時 < 60ms,這比第一次調(diào)優(yōu)時remark平均耗時495ms好得多了。
總結(jié)
解決abortable preclean 時間過長的方案可以歸結(jié)為兩步:
縮短abortable preclean 時長,通過調(diào)整這兩個參數(shù):
-XX:CMSMaxAbortablePrecleanTime=xxx
-XX:CMSScheduleRemarkEdenPenetration=xxx
復(fù)制代碼調(diào)整為多少的一個判斷標(biāo)準(zhǔn)是:abortable preclean階段結(jié)束時,新生代的空間占用不能大于某個參考值。在前面第一次調(diào)優(yōu)后,新生代(YG)占用181.274M,remark耗時80ms;新生代(YG)占用773.427M時,remark耗時910ms。所以這個參考值可以是300M。
而如果新生代增長過快,像這次調(diào)優(yōu)應(yīng)用2秒內(nèi)就能用光2G新生代堆空間的,就只能通過CMSScavengeBeforeRemark做一次Minor GC了。
增加CMSScavengeBeforeRemark參數(shù)開啟remark前進行Minor GC的嘗試
雖然官方說明這個增加這個參數(shù)是嘗試進行Minor GC,不一定會進行。但實際使用起來,幾乎每次remark前都會Minor GC。
作者:李浩宇A(yù)lex
鏈接:https://juejin.cn/post/6975125806305509389
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
