前提概要
如果沒有冬天,春天不會如此悅人;如果沒有偶爾的不幸,幸運不會如此受人歡迎。
CMS垃圾回收的6個重要階段
- initial-mark 初始标記(CMS的第一個STW階段),标記GC Root直接引用的對象,GC Root直接引用的對象不多,是以很快。
- concurrent-mark并發标記階段,由第一階段标記過的對象出發,所有可達的對象都在本階段标記。
- concurrent-preclean 并發預清理階段,也是一個并發執行的階段。在本階段,會查找前一階段執行過程中,[從新生代晉升或新配置設定或被更新的對象]。通過并發地重新掃描這些對象,預清理階段可以減少下一個stop-the-world 重新标記階段的工作量。
- concurrent-abortable-preclean,并發可中止的預清理階段。這個階段其實跟上一個階段做的東西一樣,也是為了減少下一個STW重新标記階段的工作量。增加這一階段是為了讓我們可以控制這個階段的結束時機,比如掃描多長時間(預設5秒)或者Eden區使用占比達到期望比例(預設50%)就結束本階段。
- remark重标記階段(CMS的第二個STW階段),暫停所有使用者線程,從GC Root開始重新掃描整堆,标記存活的對象。需要注意的是,雖然CMS隻回收老年代的垃圾對象,但是這個階段依然需要掃描新生代,因為很多GC Root都在新生代,而這些GC Root指向的對象又在老年代,這稱為“跨代引用”。
- concurrent-sweep ,并發清理。
分析
分析其GC日志,發現GC發生在CMS的收集階段。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5iYmBjM2EWNyYWOkJjN1YGMlljZjFWZ0QzYiFDNlVDOlVTYk1Cc19CX0VmbjN3bvwFdl5mLh5WaoN2cv5yZtl2Yz92Lc9CX6MHc0RHaiojIsJye.png)
- 箭頭1 顯示abortable-preclean階段耗時4.04秒。
- 箭頭2 顯示的是remark階段,耗時0.11秒。
- 雖然abortable-preclean階段是concurrent的,不會暫停其他的使用者線程。就算不優化,可能影響也不大。
調優之前先看下該應用的GC統計資料,包括GC次數,耗時:
統計期間内(18天)發生CMS GC 69次,其中abortable preclean階段平均耗時2.45秒,final remark階段平均112ms,最大耗時170ms。
優化目标
降低abortable preclean時間,而且不增加final remark的時間(因為remark是STW的)。
JVM參數調優
第一次調優
先嘗試調低
abortable preclean
階段的時間,看看效果。
有兩個參數可以控制這個階段何時結束:
-XX:CMSMaxAbortablePrecleanTime=5000
預設值5s,代表該階段最大的持續時間
-XX:CMSScheduleRemarkEdenPenetration=50
預設值50%,代表Eden區使用比例超過50%就結束該階段進入remark
調整為最大持續時間為1s,Eden區使用占比10%,如下:
-XX:CMSMaxAbortablePrecleanTime=1000
-XX:CMSScheduleRemarkEdenPenetration=10
為什麼調整成這樣兩個值:首先每次CMS都發生在老年代使用占比達到80%時,因為這是由下面兩個參數決定的:
-XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly
這兩個設定一般配合使用,一般用于『降低CMS GC頻率或者增加頻率、減少GC時長』的需求
- -XX:CMSInitiatingOccupancyFraction=80 是指設定CMS在對記憶體占用率達到80%的時候開始GC(因為CMS會有浮動垃圾,是以一般都較早啟動GC);
- -XX:+UseCMSInitiatingOccupancyOnly :标志來指令JVM不基于運作時收集的資料來啟動CMS垃圾收集周期。
當該标志被開啟時,JVM通過CMSInitiatingOccupancyFraction的值進行每一次CMS收集,而不僅僅是第一次。(否則後續會動态控制回收門檻值)
(慎用) 是以,隻有當我們充足的理由(比如測試)并且對應用程式産生的對象的生命周期有深刻的認知時,才應該使用該标志。
老年代的增長是由于部分對象在後仍然存活,被晉升到老年代,導緻老年代使用占比增長的,也就是在每次
Minor GC
發生之前剛剛發生過一次
CMS GC
,是以在那一刻新生代的使用占比是很低的。
Minor GC
那麼我們預計這個時候盡快結束abortable preclean階段,在remark時就不需要掃描太多的Eden區對象,remark STW的時間也就不會太長。
第一次調整參數
在統計期間(17小時左右)内,發生過2次CMS GC。Abortable Preclean 平均耗時835ms,這是預期内的。但是Final Remark 平均耗時495ms(調整前是112ms),其中一次是80ms,另一次是910ms!将近1秒鐘!Remark是STW的!對于要求低延時的應用來說這是無法接受的!
[YG occupancy: 181274 K (1887488 K)] - 年輕代目前占用情況和總容量
耗時80ms的這次remark發生時(早上9點,非高峰時段),新生代(YG)占用181.274M。
remark耗時910ms的那次GC日志
[YG occupancy: 773427 K (1887488 K)]
耗時910ms的這次remark發生時(晚上10點左右,高峰時段),新生代(YG)占用773.427M。因為這個時候高峰期,新生代的占用量上升的非常快,幾乎同樣的時間内,非高峰時段僅上升到181M,但是高峰時段就上升到773M。
- 如果
階段時間太短,随後在remark時,新生代占用越大,則remark持續的時間(STW)越長。abortale preclean
- 不縮短abortale preclean耗時會出現過程gc;縮短的話,remark階段又會變長,而且是STW,更不能接受。
對于這種情況,CMS提供了CMSScavengeBeforeRemark參數,嘗試在remark階段之前進行一次Minor GC,以降低新生代的占用。
第二次調優
增加 -XX:+CMSScavengeBeforeRemark 不是沒有代價的,因為這會增加一次Minor GC停頓。是以這個方案好或者不好的判斷标準就是:增加CMSScavengeBeforeRemark參數之後的minor GC停頓時間 + remark 停頓時間如果比增加之前的remark GC停頓時間要小,這才是好的方案。
-XX:+CMSScavengeBeforeRemark: 在CMS GC前啟動一次ygc,目的在于減少old gen對ygc gen的引用,降低remark時的開銷-----一般CMS的GC耗時 80%都在remark階段
第二次調整的結果
在統計期間(20小時左右)内,發生3次CMS GC。Abortable preclean 平均耗時693ms。Final remark平均耗時50ms,最大耗時60ms。Final remark的時間比調優前的平均時間(112ms)更低。
3次CMS GC remark前的Minor GC日志分析
第1次是非高峰時段的表現,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,這比第一次調優時remark平均耗時495ms好得多了。
總結
解決abortable preclean 時間過長的方案可以歸結為兩步:
縮短abortable preclean 時長,通過調整這兩個參數:
-XX:CMSMaxAbortablePrecleanTime=xxx
-XX:CMSScheduleRemarkEdenPenetration=xxx
- 而如果新生代增長過快,像這次調優應用2秒内就能用光2G新生代堆空間的,就隻能通過CMSScavengeBeforeRemark做一次Minor GC了。