一、概述
-
簡介:
- CMS收集器是一種以獲取最短回收停頓爲目標的收集器。
- 由於GC線程在耗時最長的併發標記階段、併發清除階段、併發重設階段都是與用戶線程一起工作的,所以從總體上來說,CMS收集器的gc線程是與用戶線程併發執行的。
-
觸發時機:
-
第一次啓動的時機:
- 老年代佔用率達到CMSInitiatingOccupancyFraction值(默認爲92%)時,jvm會啓動第一次CMS GC。
-
之後啓動的時機:
- jvm自動判斷。
-
參數:
- -XX:+UseCMSInitiatingOccupancyOnly
- 命令JVM不要根據運行時收集的數據來判斷什麼時候開始gc,而是根據CMSInitiatingOccupancyFraction的值來判斷是否進行CMS收集,一般不會打開該開關。
- -XX:CMSInitiatingOccupancyFraction
- 設置觸發第一次CMS GC的閾值,老年代佔用率達到該值時,jvm會啓動第一次CMS GC,默認爲92%。
- -XX:+UseCMSInitiatingOccupancyOnly
-
-
收集算法:
- 使用標記-清除算法。
-
GC類型:
- major gc:只收集老年代。
-
收集範圍
- 整個老年代。
二、CMS GC過程:
1、初始標記(CMS-initial-mark):
-
標記對象:
- 一個對象被標記,說明這個對象還存活着,不能被收集器回收。
-
哪些對象會被標記:
- 老年代中所有被根對象(GC Roots)直接引用的對象會被標記。
- 老年代中被年輕代中存活對象直接引用的對象會被標記。
-
標記方式:
- 該階段需要Stop the word,因爲僅標記少量節點,所以該階段帶來的暫停時間很短。
-
圖示:
2、併發標記(CMS-concurrent-mark):
-
哪些對象會被標記:
- 從初始標記階段標記的對象出發,沿着引用鏈往下檢索(RootsTracing),標記出引用鏈上所有老年代中存活的對象。
-
標記方式:
- gc線程和應用程序線程是併發執行的,故在併發標記期間可能存在對象的漏標和錯標的問題。
-
對象漏標:
- 場景1:在併發標記期間有新的對象進入到老年代(新生代的對象晉升到老年代、在老年代中直接分配對象等),這些新進入老年代的對象沒有被標記。
- 場景2:在併發標記期間用戶線程修改了存活對象A的某個字段:將這個字段指向了一個未被標記過的對象B,那麼此時對象B就由(併發標記開始時的)不可達變爲可達了,但是對象B卻沒有被標記。
-
對象錯標:
- 某個存活對象(如下圖的current obj)的某個字段引用着對象A,在併發標記期間用戶線程將該字段修改爲引用對象B,那麼此時對象A就由(併發標記開始時的)可達變爲不可達了,但是對象A卻已經被標記過了。
- 某個存活對象(如下圖的current obj)的某個字段引用着對象A,在併發標記期間用戶線程將該字段修改爲引用對象B,那麼此時對象A就由(併發標記開始時的)可達變爲不可達了,但是對象A卻已經被標記過了。
-
對象漏標和錯標的解決:
- 在併發標記階段,一些對象的引用可能已經發生了變化(導致對象漏標或錯標),jvm會將這些引用發生變化的對象(包括新進入老年代的對象)所在的Card標記爲Dirty Card。
- 在併發標記階段,一些對象的引用可能已經發生了變化(導致對象漏標或錯標),jvm會將這些引用發生變化的對象(包括新進入老年代的對象)所在的Card標記爲Dirty Card。
3、併發預清理(CMS-concurrent-preclean):
-
哪些對象會被標記:
-
從Dirty Card中包含的對象開始,沿着引用鏈往下檢索(RootsTracing),標記出引用鏈上所有老年代中存活的對象。
-
-
標記方式:
- gc線程和應用程序線程併發執行。
- 併發預清理的目的是爲了減少重新標記階段的工作,進而減少STW的時間。
-
Dirty Card恢復成正常的Card:
- 當Dirty Card中所有對象的引用鏈都檢索完成後,這個Card的Dirty標識就被清除了。
- 當Dirty Card中所有對象的引用鏈都檢索完成後,這個Card的Dirty標識就被清除了。
4、可中斷的併發預清理階段( CMS-concurrent-abortable-preclean):
-
觸發條件:
- 在併發預清理階段執行完成之後,如果eden區的佔用量大於CMSScheduleRemarkEdenSizeThreshold(默認爲2M) ,則會觸發本階段執行。
-
中斷條件:
- eden區的佔用量大於CMSScheduleRemarkEdenPenetration(默認50%),則中斷本階段。
- 本階段執行時間大於 CMSMaxAbortablePrecleanTime,則中斷本階段。
-
哪些對象會被標記:
- 和併發預清理階段標記的對象一樣。
-
標記方式:
- gc線程和應用程序線程併發執行,gc線程的標記工作可以被中斷。
- 在可中斷的併發預清理階段,jvm期望發生一次minor gc,這樣年輕代中無用的對象就被回收掉了,進而可以減少remark階段掃描對象的數量。
- 併發預清理的目的同樣也是爲了減少重新標記階段的工作,進而減少STW的時間。
-
參數:
- -XX:+CMSScheduleRemarkEdenSizeThreshold
- eden區佔用量大於該值時(默認爲2M),觸發可中斷的併發預清理階段啓動。
- -XX:CMSScheduleRemarkEdenPenetration
- 表示eden區使用比例超過制定比例就結束該階段進入remark階段。
- 默認50%,即:-XX:CMSScheduleRemarkEdenPenetration=50
- -XX:CMSMaxAbortablePrecleanTime
- 如果可中斷的預清理執行時間超過該值,那麼無論minor gc有沒有發生,該階段都會立即結束,然後進入remark階段,這樣是爲了避免因沒有等到minor gc而陷入無限等待。
- 默認5秒,即:-XX:CMSMaxAbortablePrecleanTime=5000
- -XX:+CMSScheduleRemarkEdenSizeThreshold
5、重新標記(CMS-remark):
-
哪些對象會被標記:
- 重新標記在併發標記期間遺漏的對象。
- 新生代對象可能持有老年代中對象的引用,故CMS-remark階段掃描對象的範圍是整個堆,故堆(新生代+老年代)中對象的數目影響了Remark階段耗時。
- 新生代:掃描新生代中所有的區域。
- 老年代:根據gc roots沿着引用鏈掃描 。
-
標記方式:
- 因爲併發預清理是併發執行的,所以對象的引用可能會發生進一步的改變,故jvm在該階段需要STW,以確保在清理之前保持一個正確的對象引用視圖。
- 一般重新標記階段的暫停時間是比較長的。
-
參數:
- -XX:+CMSScavengeBeforeRemark
- 在remark前強制進行一次minor gc,這樣在一定程度上降低了重新標記階段對“遺漏”對象的掃描時間。
- -XX:+CMSParallelRemarkEnabled
- 可以並行remark,減少暫停的時間
- -XX:+CMSScavengeBeforeRemark
6、併發清除(CMS-concurrent-sweep):
-
哪些對象會被清除:
- 未被標記的對象。
-
清除方式:
- gc線程和應用程序線程併發執行。
- 使用標記-清除法回收清除老年代的垃圾對象(未被標記的對象)
-
浮動垃圾:
- 概念:在併發清除階段產生的垃圾稱爲浮動垃圾。
- 說明:浮動垃圾在本次gc中是無法清除的,只能等到下次清理。
7、併發重設(CMS-concurrent-reset):
-
工作內容:
- 對記錄標記對象的表等數據結構做重置處理,爲下一次GC做準備。
-
工作方式:
- 併發重設線程和應用程序線程是併發執行的。
三、CMS GC失敗後的預案:
- 如果CMS運行期間預留的內存無法滿足程序需要,那麼就會出現一次“Concurrent Mode Failure”失敗,此時JVM將啓動後備預案:
- 使用Serial Old收集器重新對老年代進行gc,這樣一來,停頓時間就會很長。
四、CMS GC的兩種模式:
-
background模式:
- 觸發條件:old的內存佔比超過多少的時候就可能觸發一次background式的cms gc
- 其中background顧名思義是在後臺做的,也就是可以不影響正常的業務線程跑,這個過程會經歷CMS GC的所有階段,該暫停的暫停,該並行的並行,效率相對來說還比較高,畢竟有和業務線程並行的gc階段;
-
foreground模式:
- 觸發條件:比如業務線程請求分配內存,但是內存不夠了,於是可能觸發一次cms gc
- 這個過程就必須是要等內存分配到了線程才能繼續往下面走的,因此整個過程必須是STW的,因此CMS GC整個過程都是暫停應用的,但是爲了提高效率,它並不是每個階段都會走的,只走其中一些階段,這些省下來的階段主要是並行階段,Precleaning、AbortablePreclean,Resizing這幾個階段都不會經歷,其中sweep階段是同步的,但不管怎麼說如果走了類似foreground的cms gc,那麼整個過程業務線程都是不可用的,效率會影響挺大。
- 參考:JVM源碼分析之SystemGC完全解讀 - 你假笨
五、一次CMS GC耗時統計
六、CMS收集器的替代品