JVm - CMS垃圾回收器

 

原文鏈接: https://m.aliyun.com/yunqi/articles/54413

原文鏈接:https://www.jianshu.com/p/08f0b85ad665

 

垃圾回收器組合

垃圾回收器從線程運行情況分類有三種:

  • 串行回收,Serial回收器,單線程回收,全程stw;
  • 並行回收,名稱以Parallel開頭的回收器,多線程回收,全程stw;
  • 併發回收,cms與G1,多線程分階段回收,只有某階段會stw;

CMS垃圾回收

CMS垃圾回收特點

  • cms只會回收老年代和永久帶(1.8開始爲元數據區,需要設置CMSClassUnloadingEnabled),不會收集年輕帶;
  • cms是一種預處理垃圾回收器,它不能等到old內存用盡時回收,需要在內存用盡前,完成回收操作,否則會導致併發回收失敗;所以cms垃圾回收器開始執行回收操作,有一個觸發閾值,默認是老年代或永久帶達到92%;

CMS垃圾回收器工作原理

CMS的GC過程有6個階段(4個併發,2個暫停其它應用程序):

1. 初次標記(STW initial mark):標記老年代中所有的GC Roots引用的對象;標記老年代中被年輕代中活着的對象引用的對象(初始標記也會掃描新生代);會導致stw。


2. 併發標記(Concurrent marking):從初次標記收集到的‘根’對象引用開始,遍歷所有能被引用的對象。


3. 併發可中斷預清理(Concurrent precleaning):改變當運行第二階段時,由應用程序線程產生的對象引用,以更新第二階段的結果。標記在併發標記階段引用發生變化的對象,如果發現對象的引用發生變化,則JVM會標記堆的這個區域爲Dirty Card

 

那些能夠從Dirty Card到達的對象也被標記(標記爲存活),當標記做完後,這個Dirty Card區域就會消失。

 


4. 最終重新標記(STW remark):由於併發預處理是併發的,對象引用可能發生進一步變化。因此,應用程序線程會再一次被暫停(stw)以更新這些變化,並且在進行實際的清理之前確保一個正確的對象引用視圖。這一階段十分重要,因爲必須避免收集到仍被引用的對象。

 

5. 併發清理(Concurrent sweeping):清理垃圾對象,這個階段收集器線程和應用程序線程併發執行。

6. 併發重置(Concurrent reset):CMS清除內部狀態,爲下次回收做準備。

問題思考

1. 併發預處理階段意義何在?

併發預處理階段做的工作還是標記,與4的重標記功能相似。既然相似爲什麼要有這一步?

前面我們講過,CMS是以獲取最短停頓時間爲目的的GC。重標記需要STW(Stop The World),因此重標記的工作儘可能多的在併發階段完成來減少STW的時間。

此階段標記從新生代晉升的對象新分配到老年代的對象以及在併發階段被修改了的對象

2. 如何確定老年代的對象是活着的?

答案很簡單,通過GC ROOT TRACING可到達的對象就是活着的。

老年代進行GC時如何確保上圖中Current Obj標記爲活着的?答案是必須掃描新生代來確保。這也是爲什麼CMS雖然是老年代的gc,但仍要掃描新生代的原因。

在CMS日誌中我們可以清楚地看到掃描日誌:

[GC[YG occupancy: 820 K (6528 K)]

[Rescan (parallel) , 0.0024157 secs]

[weak refs processing, 0.0000143 secs]

[scrub string table, 0.0000258 secs] 

[1 CMS-remark: 479379K(515960K)] 480200K(522488K), 0.0025249 secs] 

[Times: user=0.01 sys=0.00, real=0.00 secs]

Rescan階段(STW remark的一個子階段)會掃描新生代和老年代中的對象。在日誌中可以看到此階段標識爲Rescan (parallel),說明此階段是並行進行的。

重點來了:全量的掃描新生代和老年代會不會很慢?肯定會。CMS號稱是停頓時間最短的GC,如此長的停頓時間肯定是不能接受的。如何解決呢?那就是必須要有一個能夠快速識別新生代和老年代活着的對象的機制

新生代垃圾回收完剩下的對象全是活着的,並且活着的對象很少。如果能在併發可中斷預清理階段發生一次Minor GC,那STW remark的時間就會縮短很多。

CMS 有兩個參數:CMSScheduleRemarkEdenSizeThresholdCMSScheduleRemarkEdenPenetration,默認值分別是2M、50%。

  • -XX:CMSScheduleRemarkEdenSizeThreshold(默認2m):控制abortable-preclean階段什麼時候開始執行,即當eden使用達到此值時,纔會開始abortable-preclean階段。
  • -XX:CMSScheduleRemarkEdenPenetratio(默認50%):控制abortable-preclean階段什麼時候結束執行。

所以兩個參數組合起來的意思是eden空間使用超過2M時啓動可中斷的併發預清理,直到eden空間使用率達到50%時中斷,進入remark階段。

那可終止的預清理要執行多長時間來保證發生一次Minor GC呢?答案是沒法保證。道理很簡單,因爲垃圾回收是JVM自動調度的,什麼時候進行GC我們控制不了。

但此階段總有一個執行時間吧。CMS提供了一個參數CMSMaxAbortablePrecleanTime ,默認爲5S。只要到了5S,不管發沒發生Minor GC,有沒有到CMSScheduleRemardEdenPenetration都會中止此階段,進入remark。

如果在5S內還是沒有執行Minor GC怎麼辦?CMS提供CMSScavengeBeforeRemark參數,使remark前強制進行一次Minor GC。

這樣做利弊都有:

  • 好的一面是減少了remark階段的停頓時間;
  • 壞的一面是Minor GC後緊跟着一個remark pause。如此一來,停頓時間也比較久。

CMS日誌如下:

7688.150: [CMS-concurrent-preclean-start]

7688.186: [CMS-concurrent-preclean: 0.034/0.035 secs]

7688.186: [CMS-concurrent-abortable-preclean-start]

7688.465: [GC 7688.465: [ParNew: 1040940K->1464K(1044544K), 0.0165840 secs] 1343593K->304365K(2093120K), 

0.0167509 secs]7690.093: [CMS-concurrent-abortable-preclean: 1.012/1.907 secs]  7690.095: [GC[YG occupancy: 522484 K (1044544 K)]

7690.095: [Rescan (parallel) , 0.3665541 secs]7690.462: [weak refs processing, 0.0003850 secs] [1 CMS-remark: 302901K(1048576K)] 825385K(2093120K), 0.3670690 secs]

7688.186啓動了可終止的預清理,在隨後的三秒內啓動了Minor GC,然後進入了Remark階段。

實際上爲了減少remark階段的STW時間,預清理階段會儘可能多做一些事情來減少remark停頓時間。remark的rescan階段是多線程的,爲了便於多線程掃描新生代。

3. 進行Minor GC時如果有老年代引用新生代,怎麼識別?

有研究表明,在所有的引用中,老年代引用新生代這種場景不足1%。

CMS將老年代的空間分成大小爲512bytes的塊,card table中的每個元素對應着一個塊。

併發標記時,如果某個對象的引用發生了變化,就標記該對象所在的塊爲 dirty card。併發預清理階段就會重新掃描該塊,將該對象引用的對象標識爲可達。

當有老年代引用新生代,對應的card table被標識爲相應的值(card table中是一個byte,有八位,約定好每一位的含義就可區分哪個是引用新生代,哪個是併發標記階段修改過的。所以,Minor GC通過掃描card table就可以很快的識別老年代引用新生代。

關於CMS的JVM參數調優

第一次調優

運營一段時間後,發現CMSGC超過一秒的情況非常多,GC日誌:

可以看出,在remark中的Rescan階段耗費了1.57秒,並且這個過程是會導致應用暫停的。問題定位在了Rescan階段。

發現在Rescan時新生代過大(4313641 K(7188480 K)),是導致Rescan慢的關鍵原因,如果能儘量保持新生代很小的時候就終止preclean階段,就可以控制住在Rescan時新生代的大小。

查看JVM參數發現-XX:CMSScheduleRemarkEdenPenetration的意思是當新生代存活對象佔EdenSpace的比例超過多少時,終止preclean階段並進入remark階段。這個參數的默認值是50%,按照現在的配置,就是7800m*50%=3900m左右,所以更改此參數設置爲:-XX:CMSScheduleRemarkEdenPenetration=1

進行壓力測試,發現remark階段的耗時確實降低了不少,說明優化有效。

第二次調優

運行幾天後觀察GC日誌(2011-09-05),發現每隔100000秒的CMSGC的峯值情況確實大大降低了,但是還是偶爾有超過1~2秒的CMSGC情況:

GC日誌:

發現concurrent-abortable-preclean階段超過了-XX:CMSMaxAbortablePrecleanTime設置的最大值10秒,所以強制終止了preclean階段而進入remark階段。而這段時間的兩次ParNew之間的間隔了17秒之多。希望的是在preclean階段產生一次MinorGC,所以將preclean的最大時長調整爲30秒:-XX:CMSMaxAbortablePrecleanTime=30000

第三次調優

運行一段時間後,發現居然出現了FullGC,大概在3~5天左右出現一次,以下是FullGC時的日誌:

發現在443310秒有promotion failed出現(新生代晉升到老生代空間不足導致的FullGC),但是此時的OldGen可以算出還剩1.45G的空間(5324800K-3871691K=1453109K),而根據gcLogViewer的統計,每次MinorGC後平均新生代晉升到老生代的內存大小僅爲58K。所以並不是OldGen空間不夠,而是OldGen的連續空間不夠造成的promotion failed。

換句話說,是由於OldGen在距離上次CMSGC後,又產生了大量內存碎片,當某個時間點在OldGen中的連續空間沒有一塊足夠58K的話,就會導致的promotion failed。

考慮如果能夠縮短CMSGC的週期,保證在出現promotion failed之前就進行CMSGC,就可以避免這個問題了。所以考慮將新生代空間縮小(相對來說就增加了老生代的空間),並且將CMSGC觸發比率降低,同時保證Survivor空間不變。所以優化參數改動如下:

-Xmn7800m -> -Xmn7020m
-XX:SurvivorRatio=8 –> -XX:SurvivorRatio=7
-XX:CMSInitiatingOccupancyFraction=80 -> -XX:CMSInitiatingOccupancyFraction=70 

第四次調優

上面的調優保持系統穩定運行了很長時間後,突然有一臺機器出現大量FullGC,觀察gc.log發現是由於持久帶滿造成的:

應對的方法爲加大持久帶,並讓持久帶也使用CMSGC方式回收:

-XX:PermSize=64m  ->  -XX:PermSize=200m
-XX:MaxPermSize=128m -> -XX:MaxPermSize=200m 
-XX:+CMSClassUnloadingEnabled

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章