天天看點

JVM調優實戰:解決CMS concurrent-abortable-preclean LongGC的問題背景JVM參數調優總結

背景

多個業務線的應用出現LongGC告警

最近一段時間,經常收到CAT報出來的Long GC告警(配置為大于3秒的為Longgc)。
JVM調優實戰:解決CMS concurrent-abortable-preclean LongGC的問題背景JVM參數調優總結
JVM調優實戰:解決CMS concurrent-abortable-preclean LongGC的問題背景JVM參數調優總結

分析前的一些JVM背景知識回顧

JVM堆記憶體劃分

JVM調優實戰:解決CMS concurrent-abortable-preclean LongGC的問題背景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 的收集階段。

JVM調優實戰:解決CMS concurrent-abortable-preclean LongGC的問題背景JVM參數調優總結

箭頭1 顯示abortable-preclean階段耗時4.04秒。箭頭2 顯示的是remark階段,耗時0.11秒。

雖然abortable-preclean階段是concurrent的,不會暫停其他的使用者線程。就算不優化,可能影響也不大。但是天>天收到各個業務線的gc報警,長久來說也不是好事。

在調優之前先看下該應用的GC統計資料,包括GC次數,耗時:

JVM調優實戰:解決CMS concurrent-abortable-preclean LongGC的問題背景JVM參數調優總結

統計期間内(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的一些基礎,才能少走彎路。
JVM調優實戰:解決CMS concurrent-abortable-preclean LongGC的問題背景JVM參數調優總結