圖解CMS垃圾回收機(jī)制,你值得擁有

簡(jiǎn)書(shū) 占小狼,轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處,謝謝!

最近在整理JVM相關(guān)的PPT,把CMS算法又過(guò)了一遍,每次閱讀源碼都能多了解一點(diǎn),繼續(xù)堅(jiān)持。

什么是CMS

CMS全稱(chēng) Concurrent Mark Sweep,是一款并發(fā)的、使用標(biāo)記-清除算法的垃圾回收器,
如果老年代使用CMS垃圾回收器,需要添加虛擬機(jī)參數(shù)-"XX:+UseConcMarkSweepGC"。

使用場(chǎng)景:

GC過(guò)程短暫停,適合對(duì)時(shí)延要求較高的服務(wù),用戶(hù)線(xiàn)程不允許長(zhǎng)時(shí)間的停頓。

缺點(diǎn):

服務(wù)長(zhǎng)時(shí)間運(yùn)行,造成嚴(yán)重的內(nèi)存碎片化。
另外,算法實(shí)現(xiàn)比較復(fù)雜(如果也算缺點(diǎn)的話(huà))

實(shí)現(xiàn)機(jī)制

根據(jù)GC的觸發(fā)機(jī)制分為:周期性O(shè)ld GC(被動(dòng))和主動(dòng)Old GC
個(gè)人理解,實(shí)在不知道怎么分才好。

周期性O(shè)ld GC

周期性O(shè)ld GC,執(zhí)行的邏輯也叫Background Collect,對(duì)老年代進(jìn)行回收,在GC日志中比較常見(jiàn),由后臺(tái)線(xiàn)程ConcurrentMarkSweepThread循環(huán)判斷(默認(rèn)2s)是否需要觸發(fā)。

觸發(fā)條件

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

晉升擔(dān)保失敗

老年代是否有足夠的空間來(lái)容納全部的新生代對(duì)象或歷史平均晉升到老年代的對(duì)象,如果不夠的話(huà),就提早進(jìn)行一次老年代的回收,防止下次進(jìn)行YGC的時(shí)候發(fā)生晉升失敗。

周期性O(shè)ld GC過(guò)程

當(dāng)條件滿(mǎn)足時(shí),采用“標(biāo)記-清理”算法對(duì)老年代進(jìn)行回收,過(guò)程可以說(shuō)很簡(jiǎn)單,標(biāo)記出存活對(duì)象,清理掉垃圾對(duì)象,但是為了實(shí)現(xiàn)整個(gè)過(guò)程的低延遲,實(shí)際算法遠(yuǎn)遠(yuǎn)沒(méi)這么簡(jiǎn)單,整個(gè)過(guò)程分為如下幾個(gè)部分:

對(duì)象在標(biāo)記過(guò)程中,根據(jù)標(biāo)記情況,分成三類(lèi):

  1. 白色對(duì)象,表示自身未被標(biāo)記;
  2. 灰色對(duì)象,表示自身被標(biāo)記,但內(nèi)部引用未被處理;
  3. 黑色對(duì)象,表示自身被標(biāo)記,內(nèi)部引用都被處理;

假設(shè)發(fā)生Background Collect時(shí),Java堆的對(duì)象分布如下:

1、InitialMarking(初始化標(biāo)記,整個(gè)過(guò)程STW)

該階段單線(xiàn)程執(zhí)行,主要分分為兩步:

  1. 標(biāo)記GC Roots可達(dá)的老年代對(duì)象;
  2. 遍歷新生代對(duì)象,標(biāo)記可達(dá)的老年代對(duì)象;

該過(guò)程結(jié)束后,對(duì)象分布如下:

2、Marking(并發(fā)標(biāo)記)

該階段GC線(xiàn)程和應(yīng)用線(xiàn)程并發(fā)執(zhí)行,遍歷InitialMarking階段標(biāo)記出來(lái)的存活對(duì)象,然后繼續(xù)遞歸標(biāo)記這些對(duì)象可達(dá)的對(duì)象。

因?yàn)樵撾A段并發(fā)執(zhí)行的,在運(yùn)行期間可能發(fā)生新生代的對(duì)象晉升到老年代、或者是直接在老年代分配對(duì)象、或者更新老年代對(duì)象的引用關(guān)系等等,對(duì)于這些對(duì)象,都是需要進(jìn)行重新標(biāo)記的,否則有些對(duì)象就會(huì)被遺漏,發(fā)生漏標(biāo)的情況。

為了提高重新標(biāo)記的效率,該階段會(huì)把上述對(duì)象所在的Card標(biāo)識(shí)為Dirty,后續(xù)只需掃描這些Dirty Card的對(duì)象,避免掃描整個(gè)老年代。

3、Precleaning(預(yù)清理)

通過(guò)參數(shù)CMSPrecleaningEnabled選擇關(guān)閉該階段,默認(rèn)啟用,主要做兩件事情:

  1. 處理新生代已經(jīng)發(fā)現(xiàn)的引用,比如在并發(fā)階段,在Eden區(qū)中分配了一個(gè)A對(duì)象,A對(duì)象引用了一個(gè)老年代對(duì)象B(這個(gè)B之前沒(méi)有被標(biāo)記),在這個(gè)階段就會(huì)標(biāo)記對(duì)象B為活躍對(duì)象。
  2. 在并發(fā)標(biāo)記階段,如果老年代中有對(duì)象內(nèi)部引用發(fā)生變化,會(huì)把所在的Card標(biāo)記為Dirty(其實(shí)這里并非使用CardTable,而是一個(gè)類(lèi)似的數(shù)據(jù)結(jié)構(gòu),叫ModUnionTalble),通過(guò)掃描這些Table,重新標(biāo)記那些在并發(fā)標(biāo)記階段引用被更新的對(duì)象(晉升到老年代的對(duì)象、原本就在老年代的對(duì)象)
4、AbortablePreclean(可中斷的預(yù)清理)

該階段發(fā)生的前提是,新生代Eden區(qū)的內(nèi)存使用量大于參數(shù)CMSScheduleRemarkEdenSizeThreshold 默認(rèn)是2M,如果新生代的對(duì)象太少,就沒(méi)有必要執(zhí)行該階段,直接執(zhí)行重新標(biāo)記階段。

為什么需要這個(gè)階段,存在的價(jià)值是什么?

因?yàn)镃MS GC的終極目標(biāo)是降低垃圾回收時(shí)的暫停時(shí)間,所以在該階段要盡最大的努力去處理那些在并發(fā)階段被應(yīng)用線(xiàn)程更新的老年代對(duì)象,這樣在暫停的重新標(biāo)記階段就可以少處理一些,暫停時(shí)間也會(huì)相應(yīng)的降低。

在該階段,主要循環(huán)的做兩件事:

  1. 處理 From 和 To 區(qū)的對(duì)象,標(biāo)記可達(dá)的老年代對(duì)象
  2. 和上一個(gè)階段一樣,掃描處理Dirty Card中的對(duì)象

當(dāng)然了,這個(gè)邏輯不會(huì)一直循環(huán)下去,打斷這個(gè)循環(huán)的條件有三個(gè):

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

如果在循環(huán)退出之前,發(fā)生了一次YGC,對(duì)于后面的Remark階段來(lái)說(shuō),大大減輕了掃描年輕代的負(fù)擔(dān),但是發(fā)生YGC并非人為控制,所以只能祈禱這5s內(nèi)可以來(lái)一次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啟動(dòng)了AbortablePreclean階段,在隨后不到2s就發(fā)生了一次YGC。

5、FinalMarking(并發(fā)重新標(biāo)記,STW過(guò)程)

該階段并發(fā)執(zhí)行,在之前的并行階段(GC線(xiàn)程和應(yīng)用線(xiàn)程同時(shí)執(zhí)行,好比你媽在打掃房間,你還在扔紙屑),可能產(chǎn)生新的引用關(guān)系如下:

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

上述對(duì)象中可能有一些已經(jīng)在Precleaning階段和AbortablePreclean階段被處理過(guò),但總存在沒(méi)來(lái)得及處理的,所以還有進(jìn)行如下的處理:

  1. 遍歷新生代對(duì)象,重新標(biāo)記
  2. 根據(jù)GC Roots,重新標(biāo)記
  3. 遍歷老年代的Dirty Card,重新標(biāo)記,這里的Dirty Card大部分已經(jīng)在clean階段處理過(guò)

在第一步驟中,需要遍歷新生代的全部對(duì)象,如果新生代的使用率很高,需要遍歷處理的對(duì)象也很多,這對(duì)于這個(gè)階段的總耗時(shí)來(lái)說(shuō),是個(gè)災(zāi)難(因?yàn)榭赡艽罅康膶?duì)象是暫時(shí)存活的,而且這些對(duì)象也可能引用大量的老年代對(duì)象,造成很多應(yīng)該回收的老年代對(duì)象而沒(méi)有被回收,遍歷遞歸的次數(shù)也增加不少),如果在AbortablePreclean階段中能夠恰好的發(fā)生一次YGC,這樣就可以避免掃描無(wú)效的對(duì)象。

如果在AbortablePreclean階段沒(méi)來(lái)得及執(zhí)行一次YGC,怎么辦?

CMS算法中提供了一個(gè)參數(shù):CMSScavengeBeforeRemark,默認(rèn)并沒(méi)有開(kāi)啟,如果開(kāi)啟該參數(shù),在執(zhí)行該階段之前,會(huì)強(qiáng)制觸發(fā)一次YGC,可以減少新生代對(duì)象的遍歷時(shí)間,回收的也更徹底一點(diǎn)。

不過(guò),這種參數(shù)有利有弊,利是降低了Remark階段的停頓時(shí)間,弊的是在新生代對(duì)象很少的情況下也多了一次YGC,最可憐的是在AbortablePreclean階段已經(jīng)發(fā)生了一次YGC,然后在該階段又傻傻的觸發(fā)一次。

所以利弊需要把握。

主動(dòng)Old GC

這個(gè)主動(dòng)Old GC的過(guò)程,觸發(fā)條件比較苛刻:

  1. YGC過(guò)程發(fā)生Promotion Failed,進(jìn)而對(duì)老年代進(jìn)行回收
  2. 比如執(zhí)行了System.gc(),前提是沒(méi)有參數(shù)ExplicitGCInvokesConcurrent
  3. 其它情況...

如果觸發(fā)了主動(dòng)Old GC,這時(shí)周期性O(shè)ld GC正在執(zhí)行,那么會(huì)奪過(guò)周期性O(shè)ld GC的執(zhí)行權(quán)(同一個(gè)時(shí)刻只能有一種在Old GC在運(yùn)行),并記錄 concurrent mode failure 或者 concurrent mode interrupted。

主動(dòng)GC開(kāi)始時(shí),需要判斷本次GC是否要對(duì)老年代的空間進(jìn)行Compact(因?yàn)殚L(zhǎng)時(shí)間的周期性GC會(huì)造成大量的碎片空間),判斷邏輯實(shí)現(xiàn)如下:

*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 */));

在三種情況下會(huì)進(jìn)行壓縮:

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

帶壓縮動(dòng)作的算法,稱(chēng)為MSC,標(biāo)記-清理-壓縮,采用單線(xiàn)程,全暫停的方式進(jìn)行垃圾收集,暫停時(shí)間很長(zhǎng)很長(zhǎng)...

那不帶壓縮動(dòng)作的算法是什么樣的呢?

不帶壓縮動(dòng)作的執(zhí)行邏輯叫Foreground Collect,整個(gè)過(guò)程相對(duì)周期性O(shè)ld GC來(lái)說(shuō),少了Precleaning和AbortablePreclean兩個(gè)階段,其它過(guò)程都差不多。

如果執(zhí)行System.gc(),而且添加了參數(shù)ExplicitGCInvokesConcurrent,這時(shí)并不屬于主動(dòng)GC,它會(huì)推進(jìn)周期性O(shè)ld GC的進(jìn)行,比如剛剛執(zhí)行過(guò)一次,并不會(huì)等2s后檢查條件,而是立馬啟動(dòng)周期性O(shè)ld GC。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容