CMS垃圾收集器
CMS是老年代垃圾收集器,在收集過程中可以與使用者執行緒併發操作。它可以與Serial收集器和Parallel New收集器搭配使用。CMS犧牲了系統的吞吐量來追求收集速度,適合追求垃圾收集速度的伺服器上。可以通過JVM啟動引數: -XX:+UseConcMarkSweepGC
來開啟CMS。
CMS收集過程
CMS 處理過程有七個步驟:
- 初始標記(CMS-initial-mark) ,會導致stw;
- 併發標記(CMS-concurrent-mark),與使用者執行緒同時執行;
- 預清理(CMS-concurrent-preclean),與使用者執行緒同時執行;
- 可被終止的預清理(CMS-concurrent-abortable-preclean) 與使用者執行緒同時執行;
- 重新標記(CMS-remark) ,會導致swt;
- 併發清除(CMS-concurrent-sweep),與使用者執行緒同時執行;
- 併發重置狀態等待下次CMS的觸發(CMS-concurrent-reset),與使用者執行緒同時執行; 其執行流程圖如下所示:
初始標記
這是CMS中兩次stop-the-world事件中的一次。這一步的作用是標記存活的物件,有兩部分:
- 標記老年代中所有的GC Roots物件,如下圖節點1;
- 標記年輕代中活著的物件引用到的老年代的物件(指的是年輕帶中還存活的引用型別物件,引用指向老年代中的物件)如下圖節點2、3;
在Java語言裡,可作為GC Roots物件的包括如下幾種: 1. 虛擬機器棧(棧楨中的本地變量表)中的引用的物件 ; 2. 方法區中的類靜態屬性引用的物件 ; 3. 方法區中的常量引用的物件 ; 4. 本地方法棧中JNI的引用的物件; ps:為了加快此階段處理速度,減少停頓時間,可以開啟初始標記並行化,-XX:+CMSParallelInitialMarkEnabled,同時調大並行標記的執行緒數,執行緒數不要超過cpu的核數。
併發標記
從“初始標記”階段標記的物件開始找出所有存活的物件; 因為是併發執行的,在執行期間會發生新生代的物件晉升到老年代、或者是直接在老年代分配物件、或者更新老年代物件的引用關係等等,對於這些物件,都是需要進行重新標記的,否則有些物件就會被遺漏,發生漏標的情況。為了提高重新標記的效率,該階段會把上述物件所在的Card標識為Dirty,後續只需掃描這些Dirty Card的物件,避免掃描整個老年代; 併發標記階段只負責將引用發生改變的Card標記為Dirty狀態,不負責處理; 如下圖所示,也就是節點1、2、3,最終找到了節點4和5。併發標記的特點是和應用程式執行緒同時執行。並不是老年代的所有存活物件都會被標記,因為標記的同時應用程式會改變一些物件的引用等。 由於這個階段是和使用者執行緒併發的,可能會導致concurrent mode failure。
預清理階段
前一個階段已經說明,不能標記出老年代全部的存活物件,是因為標記的同時應用程式會改變一些物件引用,這個階段就是用來處理前一個階段因為引用關係改變導致沒有標記到的存活物件的,它會掃描所有標記為Dirty的Card 如下圖所示,在併發清理階段,節點3的引用指向了6;則會把節點3的card標記為Dirty;
最後將6標記為存活,如下圖所示:
可終止的預處理
這個階段嘗試著去承擔下一個階段Final Remark階段足夠多的工作。這個階段持續的時間依賴好多的因素,由於這個階段是重複的做相同的事情直到發生abort的條件(比如:重複的次數、多少量的工作、持續的時間等等)之一才會停止。 ps:此階段最大持續時間為5秒,之所以可以持續5秒,另外一個原因也是為了期待這5秒內能夠發生一次ygc,清理年輕帶的引用,是的下個階段的重新標記階段,掃描年輕帶指向老年代的引用的時間減少;
重新標記
這個階段會導致第二次stop the word,該階段的任務是完成標記整個年老代的所有的存活物件。 這個階段,重新標記的記憶體範圍是整個堆,包含_young_gen和_old_gen。為什麼要掃描新生代呢,因為對於老年代中的物件,如果被新生代中的物件引用,那麼就會被視為存活物件,即使新生代的物件已經不可達了,也會使用這些不可達的物件當做cms的“gc root”,來掃描老年代; 因此對於老年代來說,引用了老年代中物件的新生代的物件,也會被老年代視作“GC ROOTS”:當此階段耗時較長的時候,可以加入引數-XX:+CMSScavengeBeforeRemark,在重新標記之前,先執行一次ygc,回收掉年輕帶的物件無用的物件,並將物件放入倖存帶或晉升到老年代,這樣再進行年輕帶掃描時,只需要掃描倖存區的物件即可,一般倖存帶非常小,這大大減少了掃描時間。由於之前的預處理階段是與使用者執行緒併發執行的,這時候可能年輕帶的物件對老年代的引用已經發生了很多改變,這個時候,remark階段要花很多時間處理這些改變,會導致很長stop the word,所以通常CMS儘量執行Final Remark階段在年輕代是足夠乾淨的時候。 另外,還可以開啟並行收集:-XX:+CMSParallelRemarkEnabled。
併發清理
通過以上5個階段的標記,老年代所有存活的物件已經被標記並且現在要通過Garbage Collector採用清掃的方式回收那些不能用的物件了。 這個階段主要是清除那些沒有標記的物件並且回收空間; 由於CMS併發清理階段使用者執行緒還在執行著,伴隨程式執行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。
使用CMS需要注意的幾點
減少remark階段停頓
一般CMS的GC耗時80%都在remark階段,如果發現remark階段停頓時間很長,可以嘗試新增該引數: -XX:+CMSScavengeBeforeRemark。 在執行remark操作之前先做一次Young GC,目的在於減少年輕代對老年代的無效引用,降低remark時的開銷。
記憶體碎片問題
CMS是基於標記-清除演算法的,CMS只會刪除無用物件,不會對記憶體做壓縮,會造成記憶體碎片,這時候我們需要用到這個引數: -XX:CMSFullGCsBeforeCompaction=n 意思是說在上一次CMS併發GC執行過後,到底還要再執行多少次full GC才會做壓縮。預設是0,也就是在預設配置下每次CMS GC頂不住了而要轉入full GC的時候都會做壓縮。 如果把CMSFullGCsBeforeCompaction配置為10,就會讓上面說的第一個條件變成每隔10次真正的full GC才做一次壓縮。
concurrent mode failure
這個異常發生在cms正在回收的時候。執行CMS GC的過程中,同時業務執行緒也在執行,當年輕帶空間滿了,執行ygc時,需要將存活的物件放入到老年代,而此時老年代空間不足,這時CMS還沒有機會回收老年帶產生的,或者在做Minor GC的時候,新生代救助空間放不下,需要放入老年代,而老年代也放不下而產生的。設定cms觸發時機有兩個引數:
- -XX:+UseCMSInitiatingOccupancyOnly
- -XX:CMSInitiatingOccupancyFraction=70
-XX:CMSInitiatingOccupancyFraction=70 是指設定CMS在對記憶體佔用率達到70%的時候開始GC。 -XX:+UseCMSInitiatingOccupancyOnly如果不指定, 只是用設定的回收閾值CMSInitiatingOccupancyFraction,則JVM僅在第一次使用設定值,後續則自動調整會導致上面的那個引數不起作用。
為什麼要有這兩個引數? 由於在垃圾收集階段使用者執行緒還需要執行,那也就還需要預留有足夠的記憶體空間給使用者執行緒使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程式運作使用。 CMS前五個階段都是標記存活物件的,除了”初始標記”和”重新標記”階段會stop the word ,其它三個階段都是與使用者執行緒一起跑的,就會出現這樣的情況gc執行緒正在標記存活物件,使用者執行緒同時向老年代提升新的物件,清理工作還沒有開始,old gen已經沒有空間容納更多物件了,這時候就會導致concurrent mode failure, 然後就會使用序列收集器回收老年代的垃圾,導致停頓的時間非常長。 CMSInitiatingOccupancyFraction引數要設定一個合理的值,設定大了,會增加concurrent mode failure發生的頻率,設定的小了,又會增加CMS頻率,所以要根據應用的執行情況來選取一個合理的值。如果發現這兩個引數設定大了會導致full gc,設定小了會導致頻繁的CMS GC,說明你的老年代空間過小,應該增加老年代空間的大小了。
promotion failed
在進行Minor GC時,Survivor Space放不下,物件只能放入老年代,而此時老年代也放不下造成的,多數是由於老年帶有足夠的空閒空間,但是由於碎片較多,新生代要轉移到老年帶的物件比較大,找不到一段連續區域存放這個物件導致的。
過早提升與提升失敗
在 Minor GC 過程中,Survivor Unused 可能不足以容納 Eden 和另一個 Survivor 中的存活物件, 那麼多餘的將被移到老年代, 稱為過早提升(Premature Promotion),這會導致老年代中短期存活物件的增長, 可能會引發嚴重的效能問題。 再進一步,如果老年代滿了, Minor GC 後會進行 Full GC, 這將導致遍歷整個堆, 稱為提升失敗(Promotion Failure)。
早提升的原因
- Survivor空間太小,容納不下全部的執行時短生命週期的物件,如果是這個原因,可以嘗試將Survivor調大,否則端生命週期的物件提升過快,導致老年代很快就被佔滿,從而引起頻繁的full gc;
- 物件太大,Survivor和Eden沒有足夠大的空間來存放這些大物件。
提升失敗原因
當提升的時候,發現老年代也沒有足夠的連續空間來容納該物件。為什麼是沒有足夠的連續空間而不是空閒空間呢?老年代容納不下提升的物件有兩種情況:
- 老年代空閒空間不夠用了;
- 老年代雖然空閒空間很多,但是碎片太多,沒有連續的空閒空間存放該物件。
解決方法
- 如果是因為記憶體碎片導致的大物件提升失敗,cms需要進行空間整理壓縮;
- 如果是因為提升過快導致的,說明Survivor 空閒空間不足,那麼可以嘗試調大 Survivor;
- 如果是因為老年代空間不夠導致的,嘗試將CMS觸發的閾值調低。
CMS相關引數
引數 | 型別 | 預設值 | 說明 |
---|---|---|---|
-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 | 對老年代進行壓縮,可以消除碎片,但是可能會帶來效能消耗 |
-XX:CMSFullGCsBeforeCompaction=n | uintx | 0 | CMS進行n次full gc後進行一次壓縮。如果n=0,每次full gc後都會進行碎片壓縮。如果n=0,每次full gc後都會進行碎片壓縮 |
–XX:+CMSIncrementalMode | boolean | false | 併發收集遞增進行,週期性把cpu資源讓給正在執行的應用 |
–XX:+CMSIncrementalPacing | boolean | false | 根據應用程式的行為自動調整每次執行的垃圾回收任務的數量 |
–XX:ParallelGCThreads=n | uintx | (ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8) | 併發回收執行緒數量 |
-XX:CMSIncrementalDutyCycleMin=n | uintx | 0 | 每次增量回收垃圾的佔總垃圾回收任務的最小比例 |
-XX:CMSIncrementalDutyCycle=n | uintx | 10 | 每次增量回收垃圾的佔總垃圾回收任務的比例 |
-XX:CMSInitiatingOccupancyFraction=n | uintx | jdk5 預設是68% jdk6預設92% | 當老年代記憶體使用達到n%,開始回收。 CMSInitiatingOccupancyFraction = (100 - MinHeapFreeRatio) + (CMSTriggerRatio * MinHeapFreeRatio / 100) |
-XX:CMSMaxAbortablePrecleanTime=n | intx | 5000 | 在CMS的preclean階段開始前,等待minor gc的最大時間。 |