CMS是老年代垃圾收集器,在收集過(guò)程中可以與用戶(hù)線(xiàn)程并發(fā)操作。它可以與Serial收集器和Parallel New收集器搭配使用。CMS犧牲了系統(tǒng)的吞吐量來(lái)追求收集速度,適合追求垃圾收集速度的服務(wù)器上??梢酝ㄟ^(guò)JVM啟動(dòng)參數(shù):-XX:+UseConcMarkSweepGC來(lái)開(kāi)啟CMS。
CMS收集過(guò)程
CMS 處理過(guò)程有七個(gè)步驟:
- 初始標(biāo)記(CMS-initial-mark) ,會(huì)導(dǎo)致stw;
- 并發(fā)標(biāo)記(CMS-concurrent-mark),與用戶(hù)線(xiàn)程同時(shí)運(yùn)行;
- 預(yù)清理(CMS-concurrent-preclean),與用戶(hù)線(xiàn)程同時(shí)運(yùn)行;
- 可被終止的預(yù)清理(CMS-concurrent-abortable-preclean) 與用戶(hù)線(xiàn)程同時(shí)運(yùn)行;
- 重新標(biāo)記(CMS-remark) ,會(huì)導(dǎo)致swt;
- 并發(fā)清除(CMS-concurrent-sweep),與用戶(hù)線(xiàn)程同時(shí)運(yùn)行;
- 并發(fā)重置狀態(tài)等待下次CMS的觸發(fā)(CMS-concurrent-reset),與用戶(hù)線(xiàn)程同時(shí)運(yùn)行;
其運(yùn)行流程圖如下所示:

初始標(biāo)記
這是CMS中兩次stop-the-world事件中的一次。這一步的作用是標(biāo)記存活的對(duì)象,有兩部分:
- 標(biāo)記老年代中所有的GC Roots對(duì)象,如下圖節(jié)點(diǎn)1;
- 標(biāo)記年輕代中活著的對(duì)象引用到的老年代的對(duì)象(指的是年輕帶中還存活的引用類(lèi)型對(duì)象,引用指向老年代中的對(duì)象)如下圖節(jié)點(diǎn)2、3;

在Java語(yǔ)言里,可作為GC Roots對(duì)象的包括如下幾種:
- 虛擬機(jī)棧(棧楨中的本地變量表)中的引用的對(duì)象 ;
- 方法區(qū)中的類(lèi)靜態(tài)屬性引用的對(duì)象 ;
- 方法區(qū)中的常量引用的對(duì)象 ;
- 本地方法棧中JNI的引用的對(duì)象;
ps:為了加快此階段處理速度,減少停頓時(shí)間,可以開(kāi)啟初始標(biāo)記并行化,-XX:+CMSParallelInitialMarkEnabled,同時(shí)調(diào)大并行標(biāo)記的線(xiàn)程數(shù),線(xiàn)程數(shù)不要超過(guò)cpu的核數(shù)。
并發(fā)標(biāo)記
從“初始標(biāo)記”階段標(biāo)記的對(duì)象開(kāi)始找出所有存活的對(duì)象;
因?yàn)槭遣l(fā)運(yùn)行的,在運(yùn)行期間會(huì)發(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è)老年代;
并發(fā)標(biāo)記階段只負(fù)責(zé)將引用發(fā)生改變的Card標(biāo)記為Dirty狀態(tài),不負(fù)責(zé)處理;
如下圖所示,也就是節(jié)點(diǎn)1、2、3,最終找到了節(jié)點(diǎn)4和5。并發(fā)標(biāo)記的特點(diǎn)是和應(yīng)用程序線(xiàn)程同時(shí)運(yùn)行。并不是老年代的所有存活對(duì)象都會(huì)被標(biāo)記,因?yàn)闃?biāo)記的同時(shí)應(yīng)用程序會(huì)改變一些對(duì)象的引用等。
由于這個(gè)階段是和用戶(hù)線(xiàn)程并發(fā)的,可能會(huì)導(dǎo)致concurrent mode failure。

預(yù)清理階段
前一個(gè)階段已經(jīng)說(shuō)明,不能標(biāo)記出老年代全部的存活對(duì)象,是因?yàn)闃?biāo)記的同時(shí)應(yīng)用程序會(huì)改變一些對(duì)象引用,這個(gè)階段就是用來(lái)處理前一個(gè)階段因?yàn)橐藐P(guān)系改變導(dǎo)致沒(méi)有標(biāo)記到的存活對(duì)象的,它會(huì)掃描所有標(biāo)記為Dirty的Card
如下圖所示,在并發(fā)清理階段,節(jié)點(diǎn)3的引用指向了6;則會(huì)把節(jié)點(diǎn)3的card標(biāo)記為Dirty;

最后將6標(biāo)記為存活,如下圖所示:

可終止的預(yù)處理
這個(gè)階段嘗試著去承擔(dān)下一個(gè)階段Final Remark階段足夠多的工作。這個(gè)階段持續(xù)的時(shí)間依賴(lài)好多的因素,由于這個(gè)階段是重復(fù)的做相同的事情直到發(fā)生abort的條件(比如:重復(fù)的次數(shù)、多少量的工作、持續(xù)的時(shí)間等等)之一才會(huì)停止。
ps:此階段最大持續(xù)時(shí)間為5秒,之所以可以持續(xù)5秒,另外一個(gè)原因也是為了期待這5秒內(nèi)能夠發(fā)生一次ygc,清理年輕帶的引用,是的下個(gè)階段的重新標(biāo)記階段,掃描年輕帶指向老年代的引用的時(shí)間減少;
重新標(biāo)記
這個(gè)階段會(huì)導(dǎo)致第二次stop the word,該階段的任務(wù)是完成標(biāo)記整個(gè)年老代的所有的存活對(duì)象。
這個(gè)階段,重新標(biāo)記的內(nèi)存范圍是整個(gè)堆,包含_young_gen和_old_gen。為什么要掃描新生代呢,因?yàn)閷?duì)于老年代中的對(duì)象,如果被新生代中的對(duì)象引用,那么就會(huì)被視為存活對(duì)象,即使新生代的對(duì)象已經(jīng)不可達(dá)了,也會(huì)使用這些不可達(dá)的對(duì)象當(dāng)做cms的“gc root”,來(lái)掃描老年代; 因此對(duì)于老年代來(lái)說(shuō),引用了老年代中對(duì)象的新生代的對(duì)象,也會(huì)被老年代視作“GC ROOTS”:當(dāng)此階段耗時(shí)較長(zhǎng)的時(shí)候,可以加入?yún)?shù)-XX:+CMSScavengeBeforeRemark,在重新標(biāo)記之前,先執(zhí)行一次ygc,回收掉年輕帶的對(duì)象無(wú)用的對(duì)象,并將對(duì)象放入幸存帶或晉升到老年代,這樣再進(jìn)行年輕帶掃描時(shí),只需要掃描幸存區(qū)的對(duì)象即可,一般幸存帶非常小,這大大減少了掃描時(shí)間。
由于之前的預(yù)處理階段是與用戶(hù)線(xiàn)程并發(fā)執(zhí)行的,這時(shí)候可能年輕帶的對(duì)象對(duì)老年代的引用已經(jīng)發(fā)生了很多改變,這個(gè)時(shí)候,remark階段要花很多時(shí)間處理這些改變,會(huì)導(dǎo)致很長(zhǎng)stop the word,所以通常CMS盡量運(yùn)行Final Remark階段在年輕代是足夠干凈的時(shí)候。
另外,還可以開(kāi)啟并行收集:-XX:+CMSParallelRemarkEnabled。
并發(fā)清理
通過(guò)以上5個(gè)階段的標(biāo)記,老年代所有存活的對(duì)象已經(jīng)被標(biāo)記并且現(xiàn)在要通過(guò)Garbage Collector采用清掃的方式回收那些不能用的對(duì)象了。
這個(gè)階段主要是清除那些沒(méi)有標(biāo)記的對(duì)象并且回收空間;
由于CMS并發(fā)清理階段用戶(hù)線(xiàn)程還在運(yùn)行著,伴隨程序運(yùn)行自然就還會(huì)有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過(guò)程之后,CMS無(wú)法在當(dāng)次收集中處理掉它們,只好留待下一次GC時(shí)再清理掉。這一部分垃圾就稱(chēng)為“浮動(dòng)垃圾”。
使用CMS需要注意的幾點(diǎn)
減少remark階段停頓
一般CMS的GC耗時(shí)80%都在remark階段,如果發(fā)現(xiàn)remark階段停頓時(shí)間很長(zhǎng),可以嘗試添加該參數(shù):
-XX:+CMSScavengeBeforeRemark。
在執(zhí)行remark操作之前先做一次Young GC,目的在于減少年輕代對(duì)老年代的無(wú)效引用,降低remark時(shí)的開(kāi)銷(xiāo)。
內(nèi)存碎片問(wèn)題
CMS是基于標(biāo)記-清除算法的,CMS只會(huì)刪除無(wú)用對(duì)象,不會(huì)對(duì)內(nèi)存做壓縮,會(huì)造成內(nèi)存碎片,這時(shí)候我們需要用到這個(gè)參數(shù):
-XX:CMSFullGCsBeforeCompaction=n
意思是說(shuō)在上一次CMS并發(fā)GC執(zhí)行過(guò)后,到底還要再執(zhí)行多少次full GC才會(huì)做壓縮。默認(rèn)是0,也就是在默認(rèn)配置下每次CMS GC頂不住了而要轉(zhuǎn)入full GC的時(shí)候都會(huì)做壓縮。 如果把CMSFullGCsBeforeCompaction配置為10,就會(huì)讓上面說(shuō)的第一個(gè)條件變成每隔10次真正的full GC才做一次壓縮。
concurrent mode failure
這個(gè)異常發(fā)生在cms正在回收的時(shí)候。執(zhí)行CMS GC的過(guò)程中,同時(shí)業(yè)務(wù)線(xiàn)程也在運(yùn)行,當(dāng)年輕帶空間滿(mǎn)了,執(zhí)行ygc時(shí),需要將存活的對(duì)象放入到老年代,而此時(shí)老年代空間不足,這時(shí)CMS還沒(méi)有機(jī)會(huì)回收老年帶產(chǎn)生的,或者在做Minor GC的時(shí)候,新生代救助空間放不下,需要放入老年代,而老年代也放不下而產(chǎn)生的。
設(shè)置cms觸發(fā)時(shí)機(jī)有兩個(gè)參數(shù):
- -XX:+UseCMSInitiatingOccupancyOnly
- -XX:CMSInitiatingOccupancyFraction=70
-XX:CMSInitiatingOccupancyFraction=70 是指設(shè)定CMS在對(duì)內(nèi)存占用率達(dá)到70%的時(shí)候開(kāi)始GC。
-XX:+UseCMSInitiatingOccupancyOnly如果不指定, 只是用設(shè)定的回收閾值CMSInitiatingOccupancyFraction,則JVM僅在第一次使用設(shè)定值,后續(xù)則自動(dòng)調(diào)整會(huì)導(dǎo)致上面的那個(gè)參數(shù)不起作用。
為什么要有這兩個(gè)參數(shù)?
由于在垃圾收集階段用戶(hù)線(xiàn)程還需要運(yùn)行,那也就還需要預(yù)留有足夠的內(nèi)存空間給用戶(hù)線(xiàn)程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿(mǎn)了再進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)作使用。
CMS前五個(gè)階段都是標(biāo)記存活對(duì)象的,除了”初始標(biāo)記”和”重新標(biāo)記”階段會(huì)stop the word ,其它三個(gè)階段都是與用戶(hù)線(xiàn)程一起跑的,就會(huì)出現(xiàn)這樣的情況gc線(xiàn)程正在標(biāo)記存活對(duì)象,用戶(hù)線(xiàn)程同時(shí)向老年代提升新的對(duì)象,清理工作還沒(méi)有開(kāi)始,old gen已經(jīng)沒(méi)有空間容納更多對(duì)象了,這時(shí)候就會(huì)導(dǎo)致concurrent mode failure, 然后就會(huì)使用串行收集器回收老年代的垃圾,導(dǎo)致停頓的時(shí)間非常長(zhǎng)。
CMSInitiatingOccupancyFraction參數(shù)要設(shè)置一個(gè)合理的值,設(shè)置大了,會(huì)增加concurrent mode failure發(fā)生的頻率,設(shè)置的小了,又會(huì)增加CMS頻率,所以要根據(jù)應(yīng)用的運(yùn)行情況來(lái)選取一個(gè)合理的值。如果發(fā)現(xiàn)這兩個(gè)參數(shù)設(shè)置大了會(huì)導(dǎo)致full gc,設(shè)置小了會(huì)導(dǎo)致頻繁的CMS GC,說(shuō)明你的老年代空間過(guò)小,應(yīng)該增加老年代空間的大小了。
promotion failed
在進(jìn)行Minor GC時(shí),Survivor Space放不下,對(duì)象只能放入老年代,而此時(shí)老年代也放不下造成的,多數(shù)是由于老年帶有足夠的空閑空間,但是由于碎片較多,新生代要轉(zhuǎn)移到老年帶的對(duì)象比較大,找不到一段連續(xù)區(qū)域存放這個(gè)對(duì)象導(dǎo)致的。
過(guò)早提升與提升失敗
在 Minor GC 過(guò)程中,Survivor Unused 可能不足以容納 Eden 和另一個(gè) Survivor 中的存活對(duì)象, 那么多余的將被移到老年代, 稱(chēng)為過(guò)早提升(Premature Promotion),這會(huì)導(dǎo)致老年代中短期存活對(duì)象的增長(zhǎng), 可能會(huì)引發(fā)嚴(yán)重的性能問(wèn)題。 再進(jìn)一步,如果老年代滿(mǎn)了, Minor GC 后會(huì)進(jìn)行 Full GC, 這將導(dǎo)致遍歷整個(gè)堆, 稱(chēng)為提升失?。≒romotion Failure)。
早提升的原因
- Survivor空間太小,容納不下全部的運(yùn)行時(shí)短生命周期的對(duì)象,如果是這個(gè)原因,可以嘗試將Survivor調(diào)大,否則端生命周期的對(duì)象提升過(guò)快,導(dǎo)致老年代很快就被占滿(mǎn),從而引起頻繁的full gc;
- 對(duì)象太大,Survivor和Eden沒(méi)有足夠大的空間來(lái)存放這些大對(duì)象。
提升失敗原因
當(dāng)提升的時(shí)候,發(fā)現(xiàn)老年代也沒(méi)有足夠的連續(xù)空間來(lái)容納該對(duì)象。為什么是沒(méi)有足夠的連續(xù)空間而不是空閑空間呢?老年代容納不下提升的對(duì)象有兩種情況:
- 老年代空閑空間不夠用了;
- 老年代雖然空閑空間很多,但是碎片太多,沒(méi)有連續(xù)的空閑空間存放該對(duì)象。
解決方法
- 如果是因?yàn)閮?nèi)存碎片導(dǎo)致的大對(duì)象提升失敗,cms需要進(jìn)行空間整理壓縮;
- 如果是因?yàn)樘嵘^(guò)快導(dǎo)致的,說(shuō)明Survivor 空閑空間不足,那么可以嘗試調(diào)大 Survivor;
- 如果是因?yàn)槔夏甏臻g不夠?qū)е碌?,嘗試將CMS觸發(fā)的閾值調(diào)低。
CMS相關(guān)參數(shù)
| 參數(shù) | 類(lèi)型 | 默認(rèn)值 | 說(shuō)明 |
|---|---|---|---|
| -XX:+UseConcMarkSweepGC | boolean | false | 老年代采用CMS收集器收集 |
| -XX:+CMSScavengeBeforeRemark | boolean | false | The CMSScavengeBeforeRemark forces scavenge invocation from the CMS-remark phase (from within the VM thread as the CMS-remark operation is executed in the foreground collector). |
| -XX:+UseCMSCompactAtFullCollection | boolean | false | 對(duì)老年代進(jìn)行壓縮,可以消除碎片,但是可能會(huì)帶來(lái)性能消耗 |
| -XX:CMSFullGCsBeforeCompaction=n | uintx | 0 | CMS進(jìn)行n次full gc后進(jìn)行一次壓縮。如果n=0,每次full gc后都會(huì)進(jìn)行碎片壓縮。如果n=0,每次full gc后都會(huì)進(jìn)行碎片壓縮 |
| –XX:+CMSIncrementalMode | boolean | false | 并發(fā)收集遞增進(jìn)行,周期性把cpu資源讓給正在運(yùn)行的應(yīng)用 |
| –XX:+CMSIncrementalPacing | boolean | false | 根據(jù)應(yīng)用程序的行為自動(dòng)調(diào)整每次執(zhí)行的垃圾回收任務(wù)的數(shù)量 |
| –XX:ParallelGCThreads=n | uintx | (ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8) | 并發(fā)回收線(xiàn)程數(shù)量 |
| -XX:CMSIncrementalDutyCycleMin=n | uintx | 0 | 每次增量回收垃圾的占總垃圾回收任務(wù)的最小比例 |
| -XX:CMSIncrementalDutyCycle=n | uintx | 10 | 每次增量回收垃圾的占總垃圾回收任務(wù)的比例 |
| -XX:CMSInitiatingOccupancyFraction=n | uintx | jdk5 默認(rèn)是68% jdk6默認(rèn)92% | 當(dāng)老年代內(nèi)存使用達(dá)到n%,開(kāi)始回收。CMSInitiatingOccupancyFraction = (100 - MinHeapFreeRatio) + (CMSTriggerRatio * MinHeapFreeRatio / 100)
|
| -XX:CMSMaxAbortablePrecleanTime=n | intx | 5000 | 在CMS的preclean階段開(kāi)始前,等待minor gc的最大時(shí)間。 |
總結(jié)
- CMS收集器只收集老年代,其以吞吐量為代價(jià)換取收集速度。
- CMS收集過(guò)程分為:初始標(biāo)記、并發(fā)標(biāo)記、預(yù)清理階段、可終止預(yù)清理、重新標(biāo)記和并發(fā)清理階段。其中初始標(biāo)記和重新標(biāo)記是STW的。CMS大部分時(shí)間都花費(fèi)在重新標(biāo)記階段,可以讓虛擬機(jī)先進(jìn)行一次Young GC,減少停頓時(shí)間。CMS無(wú)法解決"浮動(dòng)垃圾"問(wèn)題。
- 由于CMS的收集線(xiàn)程和用戶(hù)線(xiàn)程并發(fā),可能在收集過(guò)程中出現(xiàn)"concurrent mode failure",解決方法是讓CMS盡早GC。在一定次數(shù)的Full GC之后讓CMS對(duì)內(nèi)存做一次壓縮,減少內(nèi)存碎片,防止年輕代對(duì)象晉升到老年代時(shí)因?yàn)閮?nèi)存碎片問(wèn)題導(dǎo)致晉升失敗。
參考資料
《深入理解Java虛擬機(jī)——JVM高級(jí)特性與最佳實(shí)踐》-周志明