CMS是老年代垃圾收集器顿天,在收集過程中可以與用戶線程并發(fā)操作橄务。它可以與Serial收集器和Parallel New收集器搭配使用误债。CMS犧牲了系統(tǒng)的吞吐量來追求收集速度票编,適合追求垃圾收集速度的服務(wù)器上◇凹埽可以通過JVM啟動參數(shù):-XX:+UseConcMarkSweepGC
來開啟CMS瓣赂。
CMS收集過程
CMS 處理過程有七個步驟:
- 初始標(biāo)記(CMS-initial-mark) ,會導(dǎo)致stw;
- 并發(fā)標(biāo)記(CMS-concurrent-mark),與用戶線程同時運(yùn)行片拍;
- 預(yù)清理(CMS-concurrent-preclean)煌集,與用戶線程同時運(yùn)行;
- 可被終止的預(yù)清理(CMS-concurrent-abortable-preclean) 與用戶線程同時運(yùn)行捌省;
- 重新標(biāo)記(CMS-remark) 苫纤,會導(dǎo)致swt;
- 并發(fā)清除(CMS-concurrent-sweep)纲缓,與用戶線程同時運(yùn)行卷拘;
- 并發(fā)重置狀態(tài)等待下次CMS的觸發(fā)(CMS-concurrent-reset),與用戶線程同時運(yùn)行祝高;
其運(yùn)行流程圖如下所示:
初始標(biāo)記
這是CMS中兩次stop-the-world事件中的一次栗弟。這一步的作用是標(biāo)記存活的對象,有兩部分:
- 標(biāo)記老年代中所有的GC Roots對象褂策,如下圖節(jié)點(diǎn)1横腿;
- 標(biāo)記年輕代中活著的對象引用到的老年代的對象(指的是年輕帶中還存活的引用類型對象颓屑,引用指向老年代中的對象)如下圖節(jié)點(diǎn)2斤寂、3;
在Java語言里揪惦,可作為GC Roots對象的包括如下幾種:
- 虛擬機(jī)棧(棧楨中的本地變量表)中的引用的對象 遍搞;
- 方法區(qū)中的類靜態(tài)屬性引用的對象 ;
- 方法區(qū)中的常量引用的對象 器腋;
- 本地方法棧中JNI的引用的對象溪猿;
ps:為了加快此階段處理速度,減少停頓時間纫塌,可以開啟初始標(biāo)記并行化诊县,-XX:+CMSParallelInitialMarkEnabled,同時調(diào)大并行標(biāo)記的線程數(shù)措左,線程數(shù)不要超過cpu的核數(shù)依痊。
并發(fā)標(biāo)記
從“初始標(biāo)記”階段標(biāo)記的對象開始找出所有存活的對象;
因?yàn)槭遣l(fā)運(yùn)行的,在運(yùn)行期間會發(fā)生新生代的對象晉升到老年代怎披、或者是直接在老年代分配對象胸嘁、或者更新老年代對象的引用關(guān)系等等瓶摆,對于這些對象,都是需要進(jìn)行重新標(biāo)記的性宏,否則有些對象就會被遺漏群井,發(fā)生漏標(biāo)的情況。為了提高重新標(biāo)記的效率毫胜,該階段會把上述對象所在的Card標(biāo)識為Dirty书斜,后續(xù)只需掃描這些Dirty Card的對象,避免掃描整個老年代指蚁;
并發(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)用程序線程同時運(yùn)行。并不是老年代的所有存活對象都會被標(biāo)記枪向,因?yàn)闃?biāo)記的同時應(yīng)用程序會改變一些對象的引用等勤揩。
由于這個階段是和用戶線程并發(fā)的,可能會導(dǎo)致concurrent mode failure秘蛔。
預(yù)清理階段
前一個階段已經(jīng)說明陨亡,不能標(biāo)記出老年代全部的存活對象,是因?yàn)闃?biāo)記的同時應(yīng)用程序會改變一些對象引用深员,這個階段就是用來處理前一個階段因?yàn)橐藐P(guān)系改變導(dǎo)致沒有標(biāo)記到的存活對象的负蠕,它會掃描所有標(biāo)記為Dirty的Card
如下圖所示,在并發(fā)清理階段倦畅,節(jié)點(diǎn)3的引用指向了6遮糖;則會把節(jié)點(diǎn)3的card標(biāo)記為Dirty;
最后將6標(biāo)記為存活,如下圖所示:
可終止的預(yù)處理
這個階段嘗試著去承擔(dān)下一個階段Final Remark階段足夠多的工作叠赐。這個階段持續(xù)的時間依賴好多的因素欲账,由于這個階段是重復(fù)的做相同的事情直到發(fā)生abort的條件(比如:重復(fù)的次數(shù)、多少量的工作芭概、持續(xù)的時間等等)之一才會停止赛不。
ps:此階段最大持續(xù)時間為5秒,之所以可以持續(xù)5秒罢洲,另外一個原因也是為了期待這5秒內(nèi)能夠發(fā)生一次ygc踢故,清理年輕帶的引用,是的下個階段的重新標(biāo)記階段,掃描年輕帶指向老年代的引用的時間減少畴椰;
重新標(biāo)記
這個階段會導(dǎo)致第二次stop the word钝计,該階段的任務(wù)是完成標(biāo)記整個年老代的所有的存活對象牍疏。
這個階段挚赊,重新標(biāo)記的內(nèi)存范圍是整個堆苗踪,包含_young_gen和_old_gen。為什么要掃描新生代呢帚戳,因?yàn)閷τ诶夏甏械膶ο箸杌颍绻恍律械膶ο笠茫敲淳蜁灰暈榇婊顚ο笃危词剐律膶ο笠呀?jīng)不可達(dá)了偏友,也會使用這些不可達(dá)的對象當(dāng)做cms的“gc root”,來掃描老年代对供; 因此對于老年代來說位他,引用了老年代中對象的新生代的對象,也會被老年代視作“GC ROOTS”:當(dāng)此階段耗時較長的時候产场,可以加入?yún)?shù)-XX:+CMSScavengeBeforeRemark鹅髓,在重新標(biāo)記之前,先執(zhí)行一次ygc京景,回收掉年輕帶的對象無用的對象窿冯,并將對象放入幸存帶或晉升到老年代,這樣再進(jìn)行年輕帶掃描時确徙,只需要掃描幸存區(qū)的對象即可醒串,一般幸存帶非常小,這大大減少了掃描時間鄙皇。
由于之前的預(yù)處理階段是與用戶線程并發(fā)執(zhí)行的芜赌,這時候可能年輕帶的對象對老年代的引用已經(jīng)發(fā)生了很多改變,這個時候育苟,remark階段要花很多時間處理這些改變较鼓,會導(dǎo)致很長stop the word椎木,所以通常CMS盡量運(yùn)行Final Remark階段在年輕代是足夠干凈的時候违柏。
另外,還可以開啟并行收集:-XX:+CMSParallelRemarkEnabled香椎。
并發(fā)清理
通過以上5個階段的標(biāo)記漱竖,老年代所有存活的對象已經(jīng)被標(biāo)記并且現(xiàn)在要通過Garbage Collector采用清掃的方式回收那些不能用的對象了。
這個階段主要是清除那些沒有標(biāo)記的對象并且回收空間畜伐;
由于CMS并發(fā)清理階段用戶線程還在運(yùn)行著馍惹,伴隨程序運(yùn)行自然就還會有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過程之后,CMS無法在當(dāng)次收集中處理掉它們万矾,只好留待下一次GC時再清理掉悼吱。這一部分垃圾就稱為“浮動垃圾”。
使用CMS需要注意的幾點(diǎn)
減少remark階段停頓
一般CMS的GC耗時80%都在remark階段良狈,如果發(fā)現(xiàn)remark階段停頓時間很長后添,可以嘗試添加該參數(shù):
-XX:+CMSScavengeBeforeRemark。
在執(zhí)行remark操作之前先做一次Young GC薪丁,目的在于減少年輕代對老年代的無效引用遇西,降低remark時的開銷。
內(nèi)存碎片問題
CMS是基于標(biāo)記-清除算法的严嗜,CMS只會刪除無用對象粱檀,不會對內(nèi)存做壓縮,會造成內(nèi)存碎片漫玄,這時候我們需要用到這個參數(shù):
-XX:CMSFullGCsBeforeCompaction=n
意思是說在上一次CMS并發(fā)GC執(zhí)行過后茄蚯,到底還要再執(zhí)行多少次full GC才會做壓縮。默認(rèn)是0睦优,也就是在默認(rèn)配置下每次CMS GC頂不住了而要轉(zhuǎn)入full GC的時候都會做壓縮第队。 如果把CMSFullGCsBeforeCompaction配置為10,就會讓上面說的第一個條件變成每隔10次真正的full GC才做一次壓縮刨秆。
concurrent mode failure
這個異常發(fā)生在cms正在回收的時候凳谦。執(zhí)行CMS GC的過程中,同時業(yè)務(wù)線程也在運(yùn)行衡未,當(dāng)年輕帶空間滿了尸执,執(zhí)行ygc時,需要將存活的對象放入到老年代缓醋,而此時老年代空間不足如失,這時CMS還沒有機(jī)會回收老年帶產(chǎn)生的,或者在做Minor GC的時候送粱,新生代救助空間放不下褪贵,需要放入老年代,而老年代也放不下而產(chǎn)生的抗俄。
設(shè)置cms觸發(fā)時機(jī)有兩個參數(shù):
- -XX:+UseCMSInitiatingOccupancyOnly
- -XX:CMSInitiatingOccupancyFraction=70
-XX:CMSInitiatingOccupancyFraction=70 是指設(shè)定CMS在對內(nèi)存占用率達(dá)到70%的時候開始GC脆丁。
-XX:+UseCMSInitiatingOccupancyOnly如果不指定, 只是用設(shè)定的回收閾值CMSInitiatingOccupancyFraction,則JVM僅在第一次使用設(shè)定值,后續(xù)則自動調(diào)整會導(dǎo)致上面的那個參數(shù)不起作用。
為什么要有這兩個參數(shù)动雹?
由于在垃圾收集階段用戶線程還需要運(yùn)行槽卫,那也就還需要預(yù)留有足夠的內(nèi)存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進(jìn)行收集胰蝠,需要預(yù)留一部分空間提供并發(fā)收集時的程序運(yùn)作使用歼培。
CMS前五個階段都是標(biāo)記存活對象的震蒋,除了”初始標(biāo)記”和”重新標(biāo)記”階段會stop the word ,其它三個階段都是與用戶線程一起跑的躲庄,就會出現(xiàn)這樣的情況gc線程正在標(biāo)記存活對象查剖,用戶線程同時向老年代提升新的對象,清理工作還沒有開始噪窘,old gen已經(jīng)沒有空間容納更多對象了梗搅,這時候就會導(dǎo)致concurrent mode failure, 然后就會使用串行收集器回收老年代的垃圾效览,導(dǎo)致停頓的時間非常長无切。
CMSInitiatingOccupancyFraction參數(shù)要設(shè)置一個合理的值,設(shè)置大了丐枉,會增加concurrent mode failure發(fā)生的頻率哆键,設(shè)置的小了,又會增加CMS頻率瘦锹,所以要根據(jù)應(yīng)用的運(yùn)行情況來選取一個合理的值籍嘹。如果發(fā)現(xiàn)這兩個參數(shù)設(shè)置大了會導(dǎo)致full gc,設(shè)置小了會導(dǎo)致頻繁的CMS GC弯院,說明你的老年代空間過小辱士,應(yīng)該增加老年代空間的大小了。
promotion failed
在進(jìn)行Minor GC時听绳,Survivor Space放不下颂碘,對象只能放入老年代,而此時老年代也放不下造成的椅挣,多數(shù)是由于老年帶有足夠的空閑空間头岔,但是由于碎片較多,新生代要轉(zhuǎn)移到老年帶的對象比較大,找不到一段連續(xù)區(qū)域存放這個對象導(dǎo)致的鼠证。
過早提升與提升失敗
在 Minor GC 過程中峡竣,Survivor Unused 可能不足以容納 Eden 和另一個 Survivor 中的存活對象, 那么多余的將被移到老年代量九, 稱為過早提升(Premature Promotion),這會導(dǎo)致老年代中短期存活對象的增長适掰, 可能會引發(fā)嚴(yán)重的性能問題。 再進(jìn)一步荠列,如果老年代滿了类浪, Minor GC 后會進(jìn)行 Full GC, 這將導(dǎo)致遍歷整個堆弯予, 稱為提升失斊莼隆(Promotion Failure)个曙。
早提升的原因
- Survivor空間太小锈嫩,容納不下全部的運(yùn)行時短生命周期的對象受楼,如果是這個原因,可以嘗試將Survivor調(diào)大呼寸,否則端生命周期的對象提升過快艳汽,導(dǎo)致老年代很快就被占滿,從而引起頻繁的full gc对雪;
- 對象太大河狐,Survivor和Eden沒有足夠大的空間來存放這些大對象。
提升失敗原因
當(dāng)提升的時候瑟捣,發(fā)現(xiàn)老年代也沒有足夠的連續(xù)空間來容納該對象馋艺。為什么是沒有足夠的連續(xù)空間而不是空閑空間呢?老年代容納不下提升的對象有兩種情況:
- 老年代空閑空間不夠用了迈套;
- 老年代雖然空閑空間很多捐祠,但是碎片太多,沒有連續(xù)的空閑空間存放該對象桑李。
解決方法
- 如果是因?yàn)閮?nèi)存碎片導(dǎo)致的大對象提升失敗踱蛀,cms需要進(jìn)行空間整理壓縮;
- 如果是因?yàn)樘嵘^快導(dǎo)致的贵白,說明Survivor 空閑空間不足率拒,那么可以嘗試調(diào)大 Survivor;
- 如果是因?yàn)槔夏甏臻g不夠?qū)е碌慕模瑖L試將CMS觸發(fā)的閾值調(diào)低猬膨。
CMS相關(guān)參數(shù)
參數(shù) | 類型 | 默認(rèn)值 | 說明 |
---|---|---|---|
-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 | 對老年代進(jìn)行壓縮,可以消除碎片呛伴,但是可能會帶來性能消耗 |
-XX:CMSFullGCsBeforeCompaction=n | uintx | 0 | CMS進(jìn)行n次full gc后進(jìn)行一次壓縮寥掐。如果n=0,每次full gc后都會進(jìn)行碎片壓縮。如果n=0,每次full gc后都會進(jìn)行碎片壓縮 |
–XX:+CMSIncrementalMode | boolean | false | 并發(fā)收集遞增進(jìn)行磷蜀,周期性把cpu資源讓給正在運(yùn)行的應(yīng)用 |
–XX:+CMSIncrementalPacing | boolean | false | 根據(jù)應(yīng)用程序的行為自動調(diào)整每次執(zhí)行的垃圾回收任務(wù)的數(shù)量 |
–XX:ParallelGCThreads=n | uintx | (ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8) | 并發(fā)回收線程數(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%,開始回收召耘。CMSInitiatingOccupancyFraction = (100 - MinHeapFreeRatio) + (CMSTriggerRatio * MinHeapFreeRatio / 100)
|
-XX:CMSMaxAbortablePrecleanTime=n | intx | 5000 | 在CMS的preclean階段開始前,等待minor gc的最大時間褐隆。 |
總結(jié)
- CMS收集器只收集老年代污它,其以吞吐量為代價換取收集速度。
- CMS收集過程分為:初始標(biāo)記庶弃、并發(fā)標(biāo)記衫贬、預(yù)清理階段、可終止預(yù)清理歇攻、重新標(biāo)記和并發(fā)清理階段固惯。其中初始標(biāo)記和重新標(biāo)記是STW的。CMS大部分時間都花費(fèi)在重新標(biāo)記階段缴守,可以讓虛擬機(jī)先進(jìn)行一次Young GC葬毫,減少停頓時間镇辉。CMS無法解決"浮動垃圾"問題。
- 由于CMS的收集線程和用戶線程并發(fā)贴捡,可能在收集過程中出現(xiàn)"concurrent mode failure"忽肛,解決方法是讓CMS盡早GC。在一定次數(shù)的Full GC之后讓CMS對內(nèi)存做一次壓縮烂斋,減少內(nèi)存碎片屹逛,防止年輕代對象晉升到老年代時因?yàn)閮?nèi)存碎片問題導(dǎo)致晉升失敗。
參考資料
《深入理解Java虛擬機(jī)——JVM高級特性與最佳實(shí)踐》-周志明