背景
多個業務線的應用出現LongGC告警
最近一段時間,經常收到CAT報出來的Long GC告警(配置為大于3秒的為Longgc)。

分析前的一些JVM背景知識回顧
JVM堆記憶體劃分
-
新生代(Young Generation)
新生代内被劃分為三個區:Eden,from survivor,to survivor。大多數對象在新生代被建立。Minor GC針對的是新生代的垃圾回收。
-
老年代(Old Generation)
在新生代中經曆了幾次Minor GC仍然存活的對象,就會被放到老年代。Major GC針對的是老年代的垃圾回收。本文重點分析的CMS就是一種針對老年代的垃圾回收算法。另外Full GC是針對整堆(包括新生代和老年代)做垃圾回收的。
-
永久代(Perm)
主要存放已被虛拟機加載的類資訊,常量,靜态變量等資料。該區域對垃圾回收的影響不大,本文不會過多涉及。
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 ,并發清理。
分析
下面先看看出現LongGC時發生了什麼。
選取其中一個應用分析其GC日志,發現LongGC發生在CMS 的收集階段。
箭頭1 顯示abortable-preclean階段耗時4.04秒。箭頭2 顯示的是remark階段,耗時0.11秒。
雖然abortable-preclean階段是concurrent的,不會暫停其他的使用者線程。就算不優化,可能影響也不大。但是天>天收到各個業務線的gc報警,長久來說也不是好事。
在調優之前先看下該應用的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
而老年代的增長是由于部分對象在Minor GC後仍然存活,被晉升到老年代,導緻老年代使用占比增長的,也就是在每次CMS GC發生之前剛剛發生過一次Minor GC,是以在那一刻新生代的使用占比是很低的。那麼我們預計這個時候盡快結束abortable preclean階段,在remark時就不需要掃描太多的Eden區對象,remark STW的時間也就不會太長。
調整的思路是這樣了,那到底效果如何呢?
第一次調整的的結果
詳細過程請檢視原文
第二次調整的結果
小結
解決abortable preclean 時間過長的方案可以歸結為兩步:
- 縮短abortable preclean 時長,通過調整這兩個參數:
-XX:CMSMaxAbortablePrecleanTime=xxx
-XX:CMSScheduleRemarkEdenPenetration=xxx
調整為多少的一個判斷标準是:abortable preclean階段結束時,新生代的空間占用不能大于某個參考值。**在前面第一次調優後,新生代(YG)占用181.274M,remark耗時80ms;新生代(YG)占用773.427M時,remark耗時910ms。是以這個參考值可以是300M。而如果新生代增長過快,像這次調優應用2秒内就能用光2G新生代堆空間的,就隻能通過CMSScavengeBeforeRemark做一次Minor GC了。
- 增加CMSScavengeBeforeRemark參數開啟remark前進行Minor GC的嘗試。
- 雖然官方說明這個增加這個參數是嘗試進行Minor GC,不一定會進行。但實際使用起來,幾乎每次remark前都會Minor GC
詳細解決過程請檢視原文
總結
- 調優前明确目标
- 調優過程對GC名額進行資料統計分析(本文借助gceasy.io線上分析工具)來驗證效果
- 需要能看懂GC日志
- GC調優不是一個一蹴而就的事情,它是微調-觀察-再微調的過程。是以需要比較深入了解GC的一些基礎,才能少走彎路。