圖解 CMS 垃圾回收機制原理,-阿里面試題

G1 垃圾收集器 參考:G1 垃圾收集器入門html

G1 與CMS的區別 參考:CMS收集器和G1收集器優缺點面試

什麼是CMS算法

CMS全稱 ConcurrentMarkSweep,是一款併發的、使用標記-清除算法的垃圾回收器, 若是老年代使用CMS垃圾回收器,須要添加虛擬機參數-"XX:+UseConcMarkSweepGC"。數據結構

使用場景:併發

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

缺點:post

服務長時間運行,形成嚴重的內存碎片化。 另外,算法實現比較複雜(若是也算缺點的話)url

實現機制線程

根據GC的觸發機制分爲:週期性Old GC(被動)和主動Old GC,純屬我的理解,實在不知道怎麼分纔好。日誌

週期性Old GC

週期性Old GC,執行的邏輯也叫 BackgroundCollect,對老年代進行回收,在GC日誌中比較常見,由後臺線程ConcurrentMarkSweepThread循環判斷(默認2s)是否須要觸發。

觸發條件

  1. 若是沒有設置 UseCMSInitiatingOccupancyOnly,虛擬機會根據收集的數據決定是否觸發(線上環境建議帶上這個參數,否則會加大問題排查的難度)

  2. 老年代使用率達到閾值 CMSInitiatingOccupancyFraction,默認92%

  3. 永久代的使用率達到閾值 CMSInitiatingPermOccupancyFraction,默認92%,前提是開啓 CMSClassUnloadingEnabled

  4. 新生代的晉升擔保失敗

晉升擔保失敗

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

週期性Old GC過程

當條件知足時,採用「標記-清理」算法對老年代進行回收,過程能夠說很簡單,標記出存活對象,清理掉垃圾對象,可是爲了實現整個過程的低延遲,實際算法遠遠沒這麼簡單,整個過程分爲以下幾個部分:

對象在標記過程當中,根據標記狀況,分紅三類:

  1. 白色對象,表示自身未被標記;

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

  3. 黑色對象,表示自身被標記,內部引用都被處理;

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

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

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

  1. 標記GC Roots可達的老年代對象;

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

該過程結束後,對象分佈以下:

二、Marking(併發標記)

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

由於該階段併發執行的,在運行期間可能發生新生代的對象晉升到老年代、或者是直接在老年代分配對象、或者更新老年代對象的引用關係等等,對於這些對象,都是須要進行從新標記的,不然有些對象就會被遺漏,發生漏標的狀況。

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

三、Precleaning(預清理)

經過參數 CMSPrecleaningEnabled選擇關閉該階段,默認啓用,主要作兩件事情:

  1. 處理新生代已經發現的引用,好比在併發階段,在Eden區中分配了一個A對象,A對象引用了一個老年代對象B(這個B以前沒有被標記),在這個階段就會標記對象B爲活躍對象。

  2. 在併發標記階段,若是老年代中有對象內部引用發生變化,會把所在的Card標記爲Dirty(其實這裏並不是使用CardTable,而是一個相似的數據結構,叫ModUnionTalble),經過掃描這些Table,從新標記那些在併發標記階段引用被更新的對象(晉升到老年代的對象、本來就在老年代的對象)

四、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。

  1. ...

  2. 1678.150:[CMS-concurrent-preclean-start]

  3. 1678.186:[CMS-concurrent-preclean:0.044/0.055secs]

  4. 1678.186:[CMS-concurrent-abortable-preclean-start]

  5. 1678.365:[GC 1678.465:[ParNew:2080530K->1464K(2044544K),0.0127340secs]

  6. 1389293K->306572K(2093120K),

  7. 0.0167509secs]

  8. 1680.093:[CMS-concurrent-abortable-preclean:1.052/1.907secs]

  9. ....

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

五、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(),前提是添加了-XX:+ExplicitGCInvokesConcurrent參數

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

主動GC開始時,須要判斷本次GC是否要對老年代的空間進行Compact(由於長時間的週期性GC會形成大量的碎片空間),判斷邏輯實現以下:

  1. *should_compact =

  2. UseCMSCompactAtFullCollection&&

  3. ((_full_gcs_since_conc_gc >=CMSFullGCsBeforeCompaction)||

  4. GCCause::is_user_requested_gc(gch->gc_cause())||

  5. gch->incremental_collection_will_fail(true/* consult_young */));

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

  1. 其中參數 UseCMSCompactAtFullCollection(默認true)和CMSFullGCsBeforeCompaction(默認0),因此默認每次的主動GC都會對老年代的內存空間進行壓縮,就是把對象移動到內存的最左邊。

  2. 固然了,好比執行了 System.gc(),也會進行壓縮。

  3. 若是新生代的晉升擔保會失敗。

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

那不帶壓縮動做的算法是什麼樣的呢?

不帶壓縮動做的執行邏輯叫 ForegroundCollect,整個過程相對週期性Old GC來講,少了Precleaning和AbortablePreclean兩個階段,其它過程都差很少。

CMS的算法很複雜,一篇文章中不可能面面俱到,若是文章中說的有問題、或者有疑問能夠留言討論。

參考:圖解 CMS 垃圾回收機制原理,面試必知必會 !

相關文章
相關標籤/搜索