1. 背景
多個業務線的應用出現LongGC告警
最近一段時間,經常收到CAT報出來的Long GC告警(配置爲大於3秒的爲Longgc)。
2. 知識回顧
2.1 JVM堆內存劃分
新生代(Young Generation)
新生代內被劃分爲三個區:Eden,from survivor,to survivor。大多數對象在新生代被創建。Minor GC針對的是新生代的垃圾回收。
老年代(Old Generation)
在新生代中經歷了幾次Minor GC仍然存活的對象,就會被放到老年代。Major GC針對的是老年代的垃圾回收。本文重點分析的CMS就是一種針對老年代的垃圾回收算法。另外Full GC是針對整堆(包括新生代和老年代)做垃圾回收的。
永久代(Perm)
主要存放已被虛擬機加載的類信息,常量,靜態變量等數據。該區域對垃圾回收的影響不大,本文不會過多涉及。
2.2 CMS垃圾回收的6個重要階段
1、initial-mark 初始標記(CMS的第一個STW階段),標記GC Root直接引用的對象,GC Root直接引用的對象不多,所以很快。
2、concurrent-mark 併發標記階段,由第一階段標記過的對象出發,所有可達的對象都在本階段標記。
3、concurrent-preclean 併發預清理階段,也是一個併發執行的階段。在本階段,會查找前一階段執行過程中,從新生代晉升或新分配或被更新的對象。通過併發地重新掃描這些對象,預清理階段可以減少下一個stop-the-world 重新標記階段的工作量。
4、concurrent-abortable-preclean 併發可中止的預清理階段。這個階段其實跟上一個階段做的東西一樣,也是爲了減少下一個STW重新標記階段的工作量。增加這一階段是爲了讓我們可以控制這個階段的結束時機,比如掃描多長時間(默認5秒)或者Eden區使用佔比達到期望比例(默認50%)就結束本階段。
5、remark 重標記階段(CMS的第二個STW階段),暫停所有用戶線程,從GC Root開始重新掃描整堆,標記存活的對象。需要注意的是,雖然CMS只回收老年代的垃圾對象,但是這個階段依然需要掃描新生代,因爲很多GC Root都在新生代,而這些GC Root指向的對象又在老年代,這稱爲“跨代引用”。
6、concurrent-sweep ,併發清理。
3. 分析
下面先看看出現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.
4. 優化目標
降低abortable preclean 時間,而且不增加final remark的時間(因爲remark是STW的)。
5. JVM參數調優
5.1 第一次調優
先嚐試調低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的時間也就不會太長。
調整的思路是這樣了,那到底效果如何呢?
第一次調整的的結果
在統計期間(17小時左右)內,發生過2次CMS GC。Abortable Preclean 平均耗時835ms,這是預期內的。但是Final Remark 平均耗時495ms(調整前是112ms),其中一次是80ms,另一次是910ms!將近1秒鐘!Remark是STW的!對於要求低延時的應用來說這是無法接受的!
對比這兩次CMS GC的詳細GC日誌,我們發現了一些對分析問題非常有用的東西。
remark耗時80ms的那次GC日誌
[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。
這裏能得出一個有用的結論:如果abortale preclean階段時間太短,隨後在remark時,新生代佔用越大,則remark持續的時間(STW)越長。
這就陷入了兩難了,不縮短abortale preclean耗時會報longgc;縮短的話,remark階段又會變長,而且是STW,更不能接受。
對於這種情況,CMS提供了CMSScavengeBeforeRemark參數,嘗試在remark階段之前進行一次Minor GC,以降低新生代的佔用。
-XX:+CMSScavengeBeforeRemark
Enables scavenging attempts before the CMS remark step. By default, this option is disabled.
5.2 第二次調優
調優前的考慮:
增加-XX:+CMSScavengeBeforeRemark
不是沒有代價的,因爲這會增加一次Minor GC停頓。所以這個方案好或者不好的判斷標準就是:增加CMSScavengeBeforeRemark參數之後的minor GC停頓時間 + remark 停頓時間如果比增加之前的remark GC停頓時間要小,這纔是好的方案。
第二次調整的結果
在統計期間(20小時左右)內,發生3次CMS GC。Abortable preclean 平均耗時693ms。Final remark平均耗時50ms,最大耗時60ms。Final remark的時間比調優前的平均時間(112ms)更低。
那麼CMS GC前的Minor GC停頓時間又如何呢?來看看詳細的GC日誌。
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好得多了。
6. 優化結果
至此,我們最初的目標- 降低abortable preclean 時間,而且不增加final remark的時間 ,已經達到了。甚至remark的時間也縮短了。
7. 小結
解決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。
8. 總結
調優前明確目標
調優過程對GC指標進行數據統計分析(本文藉助gceasy.io在線分析工具)來驗證效果
需要能看懂GC日誌
GC調優不是一個一蹴而就的事情,它是微調-觀察-再微調的過程。所以需要比較深入瞭解GC的一些基礎,才能少走彎路。
作者:鼯鼠的柏拉圖之洞
blog.csdn.net/flysqrlboy/article/details/88679457
更多精彩推薦
☞ Google 鼓勵的 13 條代碼審查標準,建議收藏!
☞ 這些SQL錯誤用法,如果經常犯,說明你的水平還很low...☞ 爲啥不能用uuid做MySQL的主鍵?☞ 微信第 1 行代碼曝光!☞ 奇葩公司按代碼行數算工資,員工一個月提成2.6萬遭開除
最後,推薦給大家一個有趣有料的公衆號:寫代碼的渣渣鵬,回覆 面試 或 資源 送一你整套開發筆記