什么是CMS
CMS全稱?Concurrent Mark Sweep描滔,是一款并發(fā)的、使用標(biāo)記-清除算法的垃圾回收器踪古,
如果老年代使用CMS垃圾回收器含长,需要添加虛擬機(jī)參數(shù)-”XX:+UseConcMarkSweepGC”。
使用場(chǎng)景:
GC過程短暫停伏穆,適合對(duì)時(shí)延要求較高的服務(wù)拘泞,用戶線程不允許長時(shí)間的停頓。
缺點(diǎn):
服務(wù)長時(shí)間運(yùn)行枕扫,造成嚴(yán)重的內(nèi)存碎片化陪腌。
另外,算法實(shí)現(xiàn)比較復(fù)雜(如果也算缺點(diǎn)的話)
實(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日志中比較常見锻弓,由后臺(tái)線程ConcurrentMarkSweepThread循環(huán)判斷(默認(rèn)2s)是否需要觸發(fā)。
觸發(fā)條件
1蝌箍、如果沒有設(shè)置-XX:+UseCMSInitiatingOccupancyOnly青灼,虛擬機(jī)會(huì)根據(jù)收集的數(shù)據(jù)決定是否觸發(fā)(建議線上環(huán)境帶上這個(gè)參數(shù),不然會(huì)加大問題排查的難度)妓盲。
2杂拨、老年代使用率達(dá)到閾值?CMSInitiatingOccupancyFraction,默認(rèn)92%本橙。
3扳躬、永久代的使用率達(dá)到閾值?CMSInitiatingPermOccupancyFraction,默認(rèn)92%甚亭,前提是開啟?CMSClassUnloadingEnabled贷币。
4、新生代的晉升擔(dān)保失敗亏狰。
晉升擔(dān)保失敗
老年代是否有足夠的空間來容納全部的新生代對(duì)象或歷史平均晉升到老年代的對(duì)象役纹,如果不夠的話,就提早進(jìn)行一次老年代的回收暇唾,防止下次進(jìn)行YGC的時(shí)候發(fā)生晉升失敗促脉。
周期性O(shè)ld GC過程
當(dāng)條件滿足時(shí),采用“標(biāo)記-清理”算法對(duì)老年代進(jìn)行回收策州,過程可以說很簡單瘸味,標(biāo)記出存活對(duì)象,清理掉垃圾對(duì)象够挂,但是為了實(shí)現(xiàn)整個(gè)過程的低延遲旁仿,實(shí)際算法遠(yuǎn)遠(yuǎn)沒這么簡單,整個(gè)過程分為如下幾個(gè)部分:
對(duì)象在標(biāo)記過程中孽糖,根據(jù)標(biāo)記情況枯冈,分成三類:
白色對(duì)象,表示自身未被標(biāo)記办悟;
灰色對(duì)象尘奏,表示自身被標(biāo)記,但內(nèi)部引用未被處理病蛉;
黑色對(duì)象炫加,表示自身被標(biāo)記瑰煎,內(nèi)部引用都被處理;
假設(shè)發(fā)生Background Collect時(shí)琢感,Java堆的對(duì)象分布如下:
1丢间、InitialMarking(初始化標(biāo)記探熔,整個(gè)過程STW)
該階段單線程執(zhí)行驹针,主要分分為兩步:
標(biāo)記GC Roots可達(dá)的老年代對(duì)象;
遍歷新生代對(duì)象诀艰,標(biāo)記可達(dá)的老年代對(duì)象柬甥;
該過程結(jié)束后,對(duì)象分布如下:
2其垄、Marking(并發(fā)標(biāo)記)
該階段GC線程和應(yīng)用線程并發(fā)執(zhí)行苛蒲,遍歷InitialMarking階段標(biāo)記出來的存活對(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ù)清理)
通過參數(shù)CMSPrecleaningEnabled選擇關(guān)閉該階段,默認(rèn)啟用处窥,主要做兩件事情:
處理新生代已經(jīng)發(fā)現(xiàn)的引用嘱吗,比如在并發(fā)階段,在Eden區(qū)中分配了一個(gè)A對(duì)象碧库,A對(duì)象引用了一個(gè)老年代對(duì)象B(這個(gè)B之前沒有被標(biāo)記)柜与,在這個(gè)階段就會(huì)標(biāo)記對(duì)象B為活躍對(duì)象。
在并發(fā)標(biāo)記階段嵌灰,如果老年代中有對(duì)象內(nèi)部引用發(fā)生變化弄匕,會(huì)把所在的Card標(biāo)記為Dirty(其實(shí)這里并非使用CardTable,而是一個(gè)類似的數(shù)據(jù)結(jié)構(gòu)沽瞭,叫ModUnionTalble)迁匠,通過掃描這些Table,重新標(biāo)記那些在并發(fā)標(biāo)記階段引用被更新的對(duì)象(晉升到老年代的對(duì)象、原本就在老年代的對(duì)象)
4城丧、AbortablePreclean(可中斷的預(yù)清理)
該階段發(fā)生的前提是延曙,新生代Eden區(qū)的內(nèi)存使用量大于參數(shù)CMSScheduleRemarkEdenSizeThreshold?默認(rèn)是2M,如果新生代的對(duì)象太少亡哄,就沒有必要執(zhí)行該階段枝缔,直接執(zhí)行重新標(biāo)記階段。
為什么需要這個(gè)階段蚊惯,存在的價(jià)值是什么愿卸?
因?yàn)镃MS GC的終極目標(biāo)是降低垃圾回收時(shí)的暫停時(shí)間,所以在該階段要盡最大的努力去處理那些在并發(fā)階段被應(yīng)用線程更新的老年代對(duì)象截型,這樣在暫停的重新標(biāo)記階段就可以少處理一些趴荸,暫停時(shí)間也會(huì)相應(yīng)的降低。
在該階段宦焦,主要循環(huán)的做兩件事:
處理 From 和 To 區(qū)的對(duì)象发钝,標(biāo)記可達(dá)的老年代對(duì)象
和上一個(gè)階段一樣,掃描處理Dirty Card中的對(duì)象
當(dāng)然了波闹,這個(gè)邏輯不會(huì)一直循環(huán)下去酝豪,打斷這個(gè)循環(huán)的條件有三個(gè):
可以設(shè)置最多循環(huán)的次數(shù)?CMSMaxAbortablePrecleanLoops,默認(rèn)是0舔痪,意思沒有循環(huán)次數(shù)的限制寓调。
如果執(zhí)行這個(gè)邏輯的時(shí)間達(dá)到了閾值CMSMaxAbortablePrecleanTime,默認(rèn)是5s锄码,會(huì)退出循環(huán)夺英。
如果新生代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階段來說巡扇,大大減輕了掃描年輕代的負(fù)擔(dān)扭仁,但是發(fā)生YGC并非人為控制,所以只能祈禱這5s內(nèi)可以來一次YGC厅翔。
...
1678.150: [CMS-concurrent-preclean-start]
1678.186: [CMS-concurrent-preclean: 0.044/0.055secs]
1678.186: [CMS-concurrent-abortable-preclean-start]
1678.365: [GC 1678.465: [ParNew: 2080530K->1464K(2044544K), 0.0127340secs]
1389293K->306572K(2093120K),
0.0167509secs]
1680.093: [CMS-concurrent-abortable-preclean: 1.052/1.907secs]?
....
在上面GC日志中乖坠,1678.186啟動(dòng)了AbortablePreclean階段,在隨后不到2s就發(fā)生了一次YGC刀闷。
5熊泵、FinalMarking(并發(fā)重新標(biāo)記仰迁,STW過程)
該階段并發(fā)執(zhí)行,在之前的并行階段(GC線程和應(yīng)用線程同時(shí)執(zhí)行顽分,好比你媽在打掃房間徐许,你還在扔紙屑),可能產(chǎn)生新的引用關(guān)系如下:
老年代的新對(duì)象被GC Roots引用
老年代的未標(biāo)記對(duì)象被新生代對(duì)象引用
老年代已標(biāo)記的對(duì)象增加新引用指向老年代其它對(duì)象
新生代對(duì)象指向老年代引用被刪除
也許還有其它情況..
上述對(duì)象中可能有一些已經(jīng)在Precleaning階段和AbortablePreclean階段被處理過卒蘸,但總存在沒來得及處理的雌隅,所以還有進(jìn)行如下的處理:
遍歷新生代對(duì)象,重新標(biāo)記
根據(jù)GC Roots悬秉,重新標(biāo)記
遍歷老年代的Dirty Card澄步,重新標(biāo)記冰蘑,這里的Dirty Card大部分已經(jīng)在clean階段處理過
在第一步驟中和泌,需要遍歷新生代的全部對(duì)象,如果新生代的使用率很高祠肥,需要遍歷處理的對(duì)象也很多武氓,這對(duì)于這個(gè)階段的總耗時(shí)來說,是個(gè)災(zāi)難(因?yàn)榭赡艽罅康膶?duì)象是暫時(shí)存活的仇箱,而且這些對(duì)象也可能引用大量的老年代對(duì)象县恕,造成很多應(yīng)該回收的老年代對(duì)象而沒有被回收,遍歷遞歸的次數(shù)也增加不少)剂桥,如果在AbortablePreclean階段中能夠恰好的發(fā)生一次YGC忠烛,這樣就可以避免掃描無效的對(duì)象。
如果在AbortablePreclean階段沒來得及執(zhí)行一次YGC权逗,怎么辦美尸?
CMS算法中提供了一個(gè)參數(shù):CMSScavengeBeforeRemark,默認(rèn)并沒有開啟斟薇,如果開啟該參數(shù)师坎,在執(zhí)行該階段之前,會(huì)強(qiáng)制觸發(fā)一次YGC堪滨,可以減少新生代對(duì)象的遍歷時(shí)間胯陋,回收的也更徹底一點(diǎn)。
不過袱箱,這種參數(shù)有利有弊遏乔,利是降低了Remark階段的停頓時(shí)間,弊的是在新生代對(duì)象很少的情況下也多了一次YGC发笔,最可憐的是在AbortablePreclean階段已經(jīng)發(fā)生了一次YGC盟萨,然后在該階段又傻傻的觸發(fā)一次。
所以利弊需要把握筐咧。
主動(dòng)Old GC
這個(gè)主動(dòng)Old GC的過程鸯旁,觸發(fā)條件比較苛刻:
YGC過程發(fā)生Promotion Failed噪矛,進(jìn)而對(duì)老年代進(jìn)行回收
比如執(zhí)行了System.gc(),前提是沒有參數(shù)ExplicitGCInvokesConcurrent
其它情況…
如果觸發(fā)了主動(dòng)Old GC铺罢,這時(shí)周期性O(shè)ld GC正在執(zhí)行艇挨,那么會(huì)奪過周期性O(shè)ld GC的執(zhí)行權(quán)(同一個(gè)時(shí)刻只能有一種在Old GC在運(yùn)行),并記錄 concurrent mode failure 或者 concurrent mode interrupted韭赘。
主動(dòng)GC開始時(shí)缩滨,需要判斷本次GC是否要對(duì)老年代的空間進(jìn)行Compact(因?yàn)殚L時(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)行壓縮:
其中參數(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)存的最左邊。
當(dāng)然了袖牙,比如執(zhí)行了System.gc()侧巨,前提是沒有參數(shù)ExplicitGCInvokesConcurrent,也會(huì)進(jìn)行壓縮鞭达。
如果新生代的晉升擔(dān)保會(huì)失敗司忱。
帶壓縮動(dòng)作的算法,稱為MSC畴蹭,標(biāo)記-清理-壓縮坦仍,采用單線程,全暫停的方式進(jìn)行垃圾收集叨襟,暫停時(shí)間很長很長…
那不帶壓縮動(dòng)作的算法是什么樣的呢繁扎?
不帶壓縮動(dòng)作的執(zhí)行邏輯叫Foreground Collect,整個(gè)過程相對(duì)周期性O(shè)ld GC來說糊闽,少了Precleaning和AbortablePreclean兩個(gè)階段梳玫,其它過程都差不多。
如果執(zhí)行System.gc()墓怀,而且添加了參數(shù)ExplicitGCInvokesConcurrent汽纠,這時(shí)并不屬于主動(dòng)GC,它會(huì)推進(jìn)周期性O(shè)ld GC的進(jìn)行傀履,比如剛剛執(zhí)行過一次虱朵,并不會(huì)等2s后檢查條件,而是立馬啟動(dòng)周期性O(shè)ld GC钓账。
歡迎工作一到五年的Java工程師朋友們加入Java程序員開發(fā): 854393687
群內(nèi)提供免費(fèi)的Java架構(gòu)學(xué)習(xí)資料(里面有高可用碴犬、高并發(fā)、高性能及分布式梆暮、Jvm性能調(diào)優(yōu)服协、Spring源碼,MyBatis啦粹,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個(gè)知識(shí)點(diǎn)的架構(gòu)資料)合理利用自己每一分每一秒的時(shí)間來學(xué)習(xí)提升自己偿荷,不要再用"沒有時(shí)間“來掩飾自己思想上的懶惰窘游!趁年輕,使勁拼跳纳,給未來的自己一個(gè)交代忍饰!