cms垃圾回收機制


參考:

圖解CMS垃圾回收機制,你值得擁有

CMS垃圾回收器詳解

 

最近在整理JVM相關的PPT,把CMS算法又過了一遍,每次閱讀源碼都能多了解一點,繼續堅持。

什么是CMS

CMS全稱 Concurrent Mark Sweep,是一款並發的、使用標記-清除算法的垃圾回收器,
如果老年代使用CMS垃圾回收器,需要添加虛擬機參數-"XX:+UseConcMarkSweepGC"。

使用場景:

GC過程短暫停,適合對時延要求較高的服務,用戶線程不允許長時間的停頓。

缺點:

服務長時間運行,造成嚴重的內存碎片化。
另外,算法實現比較復雜(如果也算缺點的話)

實現機制

根據GC的觸發機制分為:周期性Old GC(被動)和主動Old GC
個人理解,實在不知道怎么分才好。

周期性Old GC

周期性Old GC,執行的邏輯也叫Background Collect,對老年代進行回收,在GC日志中比較常見,由后台線程ConcurrentMarkSweepThread循環判斷(默認2s)是否需要觸發。

 
 
觸發條件

1、如果沒有設置-XX:+UseCMSInitiatingOccupancyOnly,虛擬機會根據收集的數據決定是否觸發(建議線上環境帶上這個參數,不然會加大問題排查的難度)。
2、老年代使用率達到閾值 CMSInitiatingOccupancyFraction,默認92%。
3、永久代的使用率達到閾值 CMSInitiatingPermOccupancyFraction,默認92%,前提是開啟 CMSClassUnloadingEnabled
4、新生代的晉升擔保失敗。

晉升擔保失敗

老年代是否有足夠的空間來容納全部的新生代對象或歷史平均晉升到老年代的對象,如果不夠的話,就提早進行一次老年代的回收,防止下次進行YGC的時候發生晉升失敗。

周期性Old GC過程

當條件滿足時,采用“標記-清理”算法對老年代進行回收,過程可以說很簡單,標記出存活對象,清理掉垃圾對象,但是為了實現整個過程的低延遲,實際算法遠遠沒這么簡單,整個過程分為如下幾個部分:

 
 

對象在標記過程中,根據標記情況,分成三類:

  1. 白色對象,表示自身未被標記;
  2. 灰色對象,表示自身被標記,但內部引用未被處理;
  3. 黑色對象,表示自身被標記,內部引用都被處理;
 
 

假設發生Background Collect時,Java堆的對象分布如下:

 
 
1、InitialMarking(初始化標記,整個過程STW)

該階段單線程執行,主要分分為兩步:

  1. 標記GC Roots可達的老年代對象;
  2. 遍歷新生代對象,標記可達的老年代對象;

該過程結束后,對象分布如下:

 
 
2、Marking(並發標記)

該階段GC線程和應用線程並發執行,遍歷InitialMarking階段標記出來的存活對象,然后繼續遞歸標記這些對象可達的對象。

因為該階段並發執行的,在運行期間可能發生新生代的對象晉升到老年代、或者是直接在老年代分配對象、或者更新老年代對象的引用關系等等,對於這些對象,都是需要進行重新標記的,否則有些對象就會被遺漏,發生漏標的情況。

為了提高重新標記的效率,該階段會把上述對象所在的Card標識為Dirty,后續只需掃描這些Dirty Card的對象,避免掃描整個老年代。

 
 
3、Precleaning(預清理)

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

  1. 處理新生代已經發現的引用,比如在並發階段,在Eden區中分配了一個A對象,A對象引用了一個老年代對象B(這個B之前沒有被標記),在這個階段就會標記對象B為活躍對象。
  2. 在並發標記階段,如果老年代中有對象內部引用發生變化,會把所在的Card標記為Dirty(其實這里並非使用CardTable,而是一個類似的數據結構,叫ModUnionTalble),通過掃描這些Table,重新標記那些在並發標記階段引用被更新的對象(晉升到老年代的對象、原本就在老年代的對象)
4、AbortablePreclean(可中斷的預清理)

該階段發生的前提是,新生代Eden區的內存使用量大於參數CMSScheduleRemarkEdenSizeThreshold 默認是2M,如果新生代的對象太少,就沒有必要執行該階段,直接執行重新標記階段。

為什么需要這個階段,存在的價值是什么?

因為CMS GC的終極目標是降低垃圾回收時的暫停時間,所以在該階段要盡最大的努力去處理那些在並發階段被應用線程更新的老年代對象,這樣在暫停的重新標記階段就可以少處理一些,暫停時間也會相應的降低。

在該階段,主要循環的做兩件事:

  1. 處理 From 和 To 區的對象,標記可達的老年代對象
  2. 和上一個階段一樣,掃描處理Dirty Card中的對象

當然了,這個邏輯不會一直循環下去,打斷這個循環的條件有三個:

  1. 可以設置最多循環的次數 CMSMaxAbortablePrecleanLoops,默認是0,意思沒有循環次數的限制。
  2. 如果執行這個邏輯的時間達到了閾值CMSMaxAbortablePrecleanTime,默認是5s,會退出循環。
  3. 如果新生代Eden區的內存使用率達到了閾值CMSScheduleRemarkEdenPenetration,默認50%,會退出循環。(這個條件能夠成立的前提是,在進行Precleaning時,Eden區的使用率小於十分之一)

如果在循環退出之前,發生了一次YGC,對於后面的Remark階段來說,大大減輕了掃描年輕代的負擔,但是發生YGC並非人為控制,所以只能祈禱這5s內可以來一次YGC。

...
1678.150: [CMS-concurrent-preclean-start]
1678.186: [CMS-concurrent-preclean: 0.044/0.055 secs]
1678.186: [CMS-concurrent-abortable-preclean-start]
1678.365: [GC 1678.465: [ParNew: 2080530K->1464K(2044544K), 0.0127340 secs] 
1389293K->306572K(2093120K), 
0.0167509 secs]
1680.093: [CMS-concurrent-abortable-preclean: 1.052/1.907 secs]  
....

在上面GC日志中,1678.186啟動了AbortablePreclean階段,在隨后不到2s就發生了一次YGC。

5、FinalMarking(並發重新標記,STW過程)

該階段並發執行,在之前的並行階段(GC線程和應用線程同時執行,好比你媽在打掃房間,你還在扔紙屑),可能產生新的引用關系如下:

  1. 老年代的新對象被GC Roots引用
  2. 老年代的未標記對象被新生代對象引用
  3. 老年代已標記的對象增加新引用指向老年代其它對象
  4. 新生代對象指向老年代引用被刪除
  5. 也許還有其它情況..

上述對象中可能有一些已經在Precleaning階段和AbortablePreclean階段被處理過,但總存在沒來得及處理的,所以還有進行如下的處理:

  1. 遍歷新生代對象,重新標記
  2. 根據GC Roots,重新標記
  3. 遍歷老年代的Dirty Card,重新標記,這里的Dirty Card大部分已經在clean階段處理過

在第一步驟中,需要遍歷新生代的全部對象,如果新生代的使用率很高,需要遍歷處理的對象也很多,這對於這個階段的總耗時來說,是個災難(因為可能大量的對象是暫時存活的,而且這些對象也可能引用大量的老年代對象,造成很多應該回收的老年代對象而沒有被回收,遍歷遞歸的次數也增加不少),如果在AbortablePreclean階段中能夠恰好的發生一次YGC,這樣就可以避免掃描無效的對象。

如果在AbortablePreclean階段沒來得及執行一次YGC,怎么辦?

CMS算法中提供了一個參數:CMSScavengeBeforeRemark,默認並沒有開啟,如果開啟該參數,在執行該階段之前,會強制觸發一次YGC,可以減少新生代對象的遍歷時間,回收的也更徹底一點。

不過,這種參數有利有弊,利是降低了Remark階段的停頓時間,弊的是在新生代對象很少的情況下也多了一次YGC,最可憐的是在AbortablePreclean階段已經發生了一次YGC,然后在該階段又傻傻的觸發一次。

所以利弊需要把握。

主動Old GC

這個主動Old GC的過程,觸發條件比較苛刻:

  1. YGC過程發生Promotion Failed,進而對老年代進行回收
  2. 比如執行了System.gc(),前提是沒有參數ExplicitGCInvokesConcurrent
  3. 其它情況...

如果觸發了主動Old GC,這時周期性Old GC正在執行,那么會奪過周期性Old GC的執行權(同一個時刻只能有一種在Old GC在運行),並記錄 concurrent mode failure 或者 concurrent mode interrupted。

主動GC開始時,需要判斷本次GC是否要對老年代的空間進行Compact(因為長時間的周期性GC會造成大量的碎片空間),判斷邏輯實現如下:

*should_compact =
    UseCMSCompactAtFullCollection &&
    ((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) ||
     GCCause::is_user_requested_gc(gch->gc_cause()) ||
     gch->incremental_collection_will_fail(true /* consult_young */));

在三種情況下會進行壓縮:

  1. 其中參數UseCMSCompactAtFullCollection(默認true)和 CMSFullGCsBeforeCompaction(默認0),所以默認每次的主動GC都會對老年代的內存空間進行壓縮,就是把對象移動到內存的最左邊。
  2. 當然了,比如執行了System.gc(),前提是沒有參數ExplicitGCInvokesConcurrent,也會進行壓縮。
  3. 如果新生代的晉升擔保會失敗。

帶壓縮動作的算法,稱為MSC,標記-清理-壓縮,采用單線程,全暫停的方式進行垃圾收集,暫停時間很長很長...

那不帶壓縮動作的算法是什么樣的呢?

不帶壓縮動作的執行邏輯叫Foreground Collect,整個過程相對周期性Old GC來說,少了Precleaning和AbortablePreclean兩個階段,其它過程都差不多。

如果執行System.gc(),而且添加了參數ExplicitGCInvokesConcurrent,這時並不屬於主動GC,它會推進周期性Old GC的進行,比如剛剛執行過一次,並不會等2s后檢查條件,而是立馬啟動周期性Old GC。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM