當前位置:
首頁 > 最新 > CMS垃圾收集器

CMS垃圾收集器

介紹

CMS垃圾回收器的全稱是Concurrent Mark-Sweep Collector,從名字上可以看出兩點,一個是使用的是並發收集,第二個是使用的收集演算法是Mark-Sweep。從而也可以推測出該收集器的特點是低延遲並且會有浮動垃圾的問題。下面詳細介紹一下這個收集器的特點。


CMS收集器

CMS收集器是為了低延遲而生,通過儘可能的並行執行垃圾回收的幾個階段來把延遲控制到最低。CMS收集器是老年代的垃圾收集器,一般情況下會有ParNew來配合執行(默認情況下也是ParNew),ParNew也是使用並行的演算法來執行年輕代的回收。當然除此之外,你還可以選擇使用Serial收集器來收集年輕代,不過一般很少這樣使用。通過咱們說的CMS收集器是指廣義上的CMS收集器,包含以下幾個:ParNew(Young)GC + CMS(Old)GC + Serial GC 演算法(應對核心的CMS GC某些時候的不趕趟,開銷很大)。本文重點介紹一些CMS在Old區域的回收。


觸發條件

CMS垃圾收集器的觸發條件有以下幾個:

1、如果沒有設置-XX:+UseCMSInitiatingOccupancyOnly,虛擬機會根據收集的數據決定是否觸發(建議帶上這個參數)。

2、老年代使用率達到閾值 CMSInitiatingOccupancyFraction,默認92%,前提是配置了第一個參數。

3、永久代的使用率達到閾值 CMSInitiatingPermOccupancyFraction,默認92%,前提是開啟 CMSClassUnloadingEnabled並且配置了第一個參數。

4、新生代的晉陞擔保失敗。


CMS的收集階段

CMS收集器在收集老年代的時候分為以下幾個階段:初始標記、並發標記、預清理、可中斷預清理、最終標記、並發清除、並發重置。每個階段的運行過程如下:

下面分別從這幾個階段來介紹CMS收集器。


初始標記階段主要做兩件事:一是遍歷GCRoot可直達的老年代對象;二是遍歷新生代直達的老年代對象。這裡的直達是指直接關聯到GCRoot的一級對象。初始標記階段是完全STW的,引用程序會暫停。通過-XX:+CMSParallelInitialMarkEnabled參數可以開啟該階段的並行標記,使用多個線程進行標記,減少暫停時間。

哪些對象可以作為GCRoot:

1、所有Java線程當前棧幀引用的,也就是正在被調用的方法的引用類型的參數、局部變數以及臨時值。

2、所有的靜態數據結構引用的對象

3、String常量池裡的引用

4、運行時常量池裡引用的類型

並發標記階段是與應用程序一起執行的,這個階段主要做兩件事:

1、對初始標記中標記的存活對象進行trace,標記這些對象為可達對象,例如A->B,A在初始標記被識別,而B就是在並發標記階段被識別。

2、將在並發階段新生代晉陞到老年代的對象、直接在老年代分配的對象以及老年代引用關係發生變化的對象所在的card標記為dirty,避免在重新標記階段掃描整個老年代。

因為並發標記階段與引用程序一起執行,因此會出現之前A->B->C變成A->C的情況,這種情況下C對象時無法在並發標記階段被標記的。在標記階段會使用三色標記演算法。

三色標記法把 GC 中的對象劃分成三種情況:

白色:還沒有搜索過的對象(白色對象會被當成垃圾對象)

灰色:正在搜索的對象

黑色:搜索完成的對象(不會當成垃圾對象,不會被 GC)

假設剛開始的對象圖如下:

在並發標記階段,B對象引用C對象變為A對象引用C對象,如下

這時候再掃描的時候就會變成如下的圖

此時C對象變成了白色的,但是顯然是不正確的,於是就有兩種方式來處理這種情況:

1、在對象引用發生變化之前記錄對象引用關係

2、在對象引用發生變化之後記錄對象引用關係

這兩種思路正好對應了CMS和G1的兩種處理方式。在CMS採用的是增量更新(Incremental update),只要在寫屏障(write barrier)里發現要有一個白對象的引用被賦值到一個黑對象 的欄位里,那就把這個白對象變成灰色的。即插入的時候記錄下來。在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,刪除的時候記錄所有的對象


通過參數CMSPrecleaningEnabled選擇關閉該階段,默認啟用,主要做兩件事情

1、並發標記階段在Eden分配了對象A,並且A引用了老年代對象B,那麼這個階段標記B為活躍對象

2、掃描並發標記階段的Dirty Card,重新標記那些在並發標記階段引用被更新的對象


該階段存在的目的是減輕重新標記的工作量,減少暫停時間,主要做兩件事情:

1、掃描處理DirtyCard中的對象

2、處理新生代引用到的老年代的對象

該階段退出的條件有三個:

1、CMSMaxAbortablePrecleanTime參數控制的5秒退出

2、Eden區達到CMSScheduleRemarkEdenPenetration參數配置的值(默認50%)

3、CMSMaxAbortablePrecleanLoops控制的掃描次數(默認是0,不退出)

該階段是希望能發生一次Young GC,這樣就可以減少Eden區對象的數量,降低重新標記的工作量,因為重新標記會掃描整個Eden區的


最終標記又叫重新標記,該階段也是STW的,主要會遍歷三個地方:

1、遍歷Eden區,重新標記

2、遍歷DirtyCard,重新標記

3、遍歷GC Root,重新標記

由於該階段遍歷的區域很多,因此有可能會耗時比較長,並且該階段是完全的STW的。

通過CMSScavengeBeforeRemark參數可以強制在重新標記階段之前強制進行一次YoungGC,通過設置CMSParallelRemarkEnabled參數可以開啟並行的Remark,加快remark的速度。


移除那些不用的對象,回收他們佔用的空間並且為將來使用。該階段有可能產生浮動垃圾,可以通過參數UseCMSCompactAtFullCollection和CMSFullGCsBeforeCompaction來控制壓縮次數。


該階段是最後一個階段,重置CMS的數據結構。


CMS日誌說明

這是一段完整的CMS日誌:

這段日誌展示了CMS回收時的各個階段,在CMS Initial Mark階段,老年代容量是1228800K,目前使用了845243K,該階段耗時0.0195530秒,這段時間是STW的;接下來進行concurrent-mark階段,耗時0.429秒;preClean階段耗時0.009秒;再往下是abortable-preclean階段,這個階段耗時5秒,結束的原因是最長配置的該階段運行時間是5s,這個階段並沒有發生YoungGC;接下來是第二個STW,Final Remark階段,222099 K (1382400 K)分別是年輕代的佔用和總空間情況,該階段還會進行弱引用的處理、class卸載以及符號表的掃描,845243K(1228800K)是老年代的使用量和總空間,這個階段總共耗時0.1878564秒;標記完成之後就開始concurrent-sweep階段進行並發清除,然後運行concurrent-reset重置CMS數據結構。


CMS失敗處理

在運行CMS收集器的時候,可能會出現兩種類型的失敗

當老年代無法容納新生代GC晉陞的對象時發生併發模式失敗,併發模式失敗意味著CMS退化成完全STW的Full GC,也就是Serial GC。下面的圖片揭示了在發生併發模式失敗時的日誌:

針對這種情況,有兩個方面需要考慮:

1、給後台線程更多的運行機會。也就是說更早的啟動並發收集周期。CMS收集器在老年代使用佔到60%的時候啟動比佔到70%才啟動,顯然前者完成垃圾回收的幾率更大。為了實現這種配置,可以同時設置以下兩個參數:-XX:CMSInitiatingOccupancyFraction=N和-XX:+UseCMSInitiatingOccupancyOnly。如果同時設置了這兩個參數就可以讓CMS只根據老年代的使用比例來決定是否啟動CMS垃圾收集。

2、更多的線程來運行CMS。之所以出現併發模式失敗,是因為CMS的速度跑不贏對象晉陞到老年代的速度了。所以可以通過給CMS更多的線程來加快CMS的速度。可以通過-XX:ConGCThreads=N來設置後台線程的數量。默認情況下線程數ConcGCThreads=(3+ParallelGCThreads)/4,是根據ParallelGCThreads來計算的,ParallelGCThreads的值可以通過-XX:ParallelGCThreads參數來設置。並不是說設置越多的線程來運行CMS越好,因為CMS在運行的時候會完整的佔用一顆CPU,所以在CPU比較緊張的情況下,這個值還是要謹慎設置的。


老年代有足夠的空間,但是由於碎片化嚴重,無法容納新生代中晉陞的對象,發生晉陞失敗。下面的圖片揭示了在發生晉陞失敗時的日誌:

晉陞失敗的原因是碎片化嚴重,所以這個問題的解決方案就是如何減少碎片化的問題。CMS提供了兩個參數來對碎片進行整理和壓縮。-XX:+UseCMSCompactAtFullCollection這個設置的作用是在進行FullGC的時候對碎片進行整理和壓縮。-XX:CMSFullGCsBeforeCompaction=*這個參數是設置在進行多少次FullGC的時候對老年代的內存進行一次碎片整理壓縮。通過設置這兩個參數可以有效的對碎片問題進行優化。同樣需要注意的是對碎片進行整理壓縮是一個比較耗時的操作,所以也需要謹慎設置。


CMS的一些參數說明

結合上面已經講過的參數配置,下面給出CMS的一些參數說明:

-XX:+UseConcMarkSweepGC 激活CMS收集器

-XX:ConcGCThreads 設置CMS線程的數量

-XX:+UseCMSInitiatingOccupancyOnly 只根據老年代使用比例來決定是否進行CMS

-XX:CMSInitiatingOccupancyFraction 設置觸發CMS老年代回收的內存使用率佔比

-XX:+CMSParallelRemarkEnabled 並行運行最終標記階段,加快最終標記的速度

-XX:+UseCMSCompactAtFullCollection 每次觸發CMS Full GC的時候都整理一次碎片

-XX:CMSFullGCsBeforeCompaction=* 經過幾次CMS Full GC的時候整理一次碎片

-XX:+CMSClassUnloadingEnabled 讓CMS可以收集永久帶,默認不會收集

-XX:+CMSScavengeBeforeRemark 最終標記之前強制進行一個Minor GC

-XX:+ExplicitGCInvokesConcurrent 當調用System.gc()的時候,執行並行gc,只有在CMS或者G1下該參數才有效。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 雲襲Talk 的精彩文章:

TAG:雲襲Talk |