相關概念
CMS GC的官方名稱為“Mostly Concurrenct Mark and Sweep Garbage Collector”(最大-並發-標記-清除-垃圾收集器)。
作用范圍: 老年代
算法: 並發標記清除算法。
啟用參數:-XX:+UseConMarkSweepGC
默認回收線程數:(處理器核心數量 + 3)/4
Java9之后使用CMS垃圾收集器后,默認年輕代就為ParNew收集器,並且不可更改,同時JDK9之后被標記為不推薦使用,JDK14就被刪除了。
並發與並行有什么區別?
並行(Parallel):並行描述的多條垃圾收集器線程之間的關系,說明同一時間有多條這樣的線程在協同工作,通常默認此時用戶線程為等待狀態。
並發(Concurrent):並發描述的垃圾收集線程與用戶線程之間的關系,說明同一時間垃圾收集線程與用戶線程都在工作,由於用戶線程並未凍結,因此還能繼續相應服務請求,但由於垃圾收集器線程占用了一定的系統資源,此時應用程序處理的吞吐量將受到一定影響。
設計目標/優點:避免在老年代垃圾收集時出現長時間的卡頓,主要通過兩種手段來達成此目標:
- 第一,不對老年代進行整理,而是使用空閑列表(free-list)來管理內存空間的回收
- 第二,在mark-and-sweep(標記-清除)階段的大部分工作和應用線程一起並發執行。
適用場景:
- GC過程短暫,低延遲,適合對延遲要求較高的系統
如果服務器是多核CPU,並且主要調優目標是降低GC停頓導致的系統延遲,那么使用CMS是個很明智的選擇。通過減少每一次GC停頓的時間,很多時候會直接改善用戶體驗。因為多數情況下有部分CPU資源被垃圾回收器線程消耗,所以在CPU資源受限的情況下,CMS GC會比並行GC的吞吐量差一些(對於絕大部分系統,這個吞吐和延遲的差別應該都不明顯)
在實際情況中,進行老年代的並發回收時,可能會伴隨多次年輕代的minor GC。在這種情況下full GC的日志中就會摻雜着多次minor GC事件
CMS GC的幾個大階段
- 1、初始標記(CMS initial mark)
- 2、並發標記(CMS concurrent mark)
- 3、重新標記(CMS remark)
- 4、並發清除(CMS concurrent sweep)
階段1:初始標記
這個階段會STW。
工作模式: JDK7之前單線程,JDK8之后多線程
目標: 標記所有的根對象,包括根對象直接引用的對象,以及被年輕代中所有存活的對象所引用的老年代對象(只是標記一下GC Roots能直接關聯到的對象,速度很快)
階段2:並發標記
此階段,CMS GC遍歷所有的對象,標記存活的對象,從前一階段“初始標記”找到的根元素開始算起。“並發標記”階段就是與應用程序同時運行,不用暫停的階段。此階段由於與用戶線程並發執行,對象的狀態可能會發生變化,如下:
- 年輕代的對象從年輕代晉升到老年代
- 有些對象被直接分配到老年代
- 老年代和年輕代的對象引用關系變化
JVM會通過Card(卡片)
的方式將發生改變的老年代區域標記為“臟”區,這就是所謂的卡片標記(Card Marking)
在上邊的圖中,“當前處理的對象”的一個引用就被應用線程給斷開了,即這個部分的對象關系發生了變化
階段3:並發預清理
也是用於標記老年代存活的對象,此階段仍然是與應用線程並發執行的,不需要停止應用線程。
目的: 讓最終/重新標記的STW時間盡可能短
標記目標:
- 老年代中在並發標記中被標記為“dirty”的card
- 幸存區(from和to)中引用的老年代對象
關閉參數:-XX:-CMSPrecleaningEnabled
,默認開啟
階段4:可取消的並發預清理
此階段也不停止應用程序,本階段嘗試在STW的最終標記階段之前盡可能多做一些工作。本階段的具體時間取決於多種因素,因為它循環做同樣的事情,直到滿足某個退出條件(如迭代次數、有用工作量、消耗的系統時間等等)
目標: 與並發預處理一樣,為了使最終/重新標記的STW時間盡可能短
價值: 在進入最終標記前盡量等到一個Minor GC,盡量縮短最終標記階段的停頓時間
觸發條件: 在預清理步驟后,如果滿足下面這個個條件,就會開啟可中斷的預清理,直接進入重新標記階段
- Eden的使用空間大於
-XX:CMSScheduleRemarkEdenSizeThreshold
”,這個參數的默認值是2M;
取消條件:
- 設置了
CMSMaxAbortablePrecleanLoops
循環次數,並且執行的次數大於或者等於這個值的時候。默認為0 CMSMaxAbortablePrecleanTime
,執行可中斷預清理的時間超過了這個值,這個參數的默認值是5000毫秒- Eden的使用率達到
-XX:CMSScheduleRemarkEdenPenetration
,這個參數的默認值是50%。
問題: 可能在可取消的並發預處理過程中一直沒等到Minor GC,這個時候進行最終標記的話,可能會發生連續停頓,假設新生代在最終標記的時候發生了Minor GC(STW),最終標記又是STW的,因此可能會發生連續停頓,CMS提供了參數CMSScavengeBeforeRemark
使最終/重新標記前強制進行一次Minor GC(其實這樣也會導致連續停頓,Minor和Remark)。
階段5:最終標記/重標記
最終標記是此階段GC事件中的第二次(也是最后一次)STW停頓。
目標: 重新掃描堆中的對象,因為之前的預清理階段是並發執行的,有可能GC線程跟不上應用程序的修改速度。
掃描范圍: 新生代對象+GC Roots+被標記為“臟”區的對象。如果預清理階段沒有做好,這一步掃描新生代的時候就會花很多時間。
階段6:Concurrent Sweep(並發清除)
此階段與應用程序並發執行,不需要STW停頓。JVM在此階段刪除不再使用的對象,並回收他們占用的內存空間。因為階段5已經把所有還在使用的對象進行了標記,因此此階段可以與應用線程並發的執行。
階段7:Concurrent Reset(並發重置)
此階段與應用程序並發執行,重置CMS算法相關的內部數據,為下一次GC循環做准備。
總之,CMS垃圾收集器在減少停頓時間上做了很多復雜的而有用的工作,用於垃圾回收的並行線程執行的同時,並不需要暫停應用線程。
動態檢測機制
CMS會根據歷史記錄,預測老年代還有多長時間會滿及進行一次回收所需的時間,可以使用參數_-XX:+UseCMSInitiatingOccupancyOnly_
來關閉,開啟這個參數后,配置的回收閾值-XX:CMSInitiatingOccupancyFraction=N
會長期生效,否則只會第一次生效
缺點
- 吞吐量降低, 對處理器資源敏感,執行垃圾收集時會占用一部分線程時程序吞吐量降低
- 占用CPU資源,與CPU核數掛鈎, 開頭說到了CMS默認啟動的回收線程是(CPU核心數 +3)/4,當CPU核數越多,垃圾回收線程占用的資源就越少,反正CPU核數越少,占用資源就越多。
- 內存碎片問題: 由於CMS使用的是標記-清除算法,這種算法的弊端就是會產生內存碎片,導致大對象無法分配,就會觸發Full GC。
- CMS收集器提供了一個參數
-XX:+UseCMSCompactAtFullCollection(默認開啟,JDK9廢棄)
,在進行Full GC之前進行一次內存整理(無法並發,Shenandoah和ZGC可以), - 這樣空間碎片雖然解決了,但是停頓時間也增長了,CMS還提供了一個參數
-XX:CMSFullGCBeforeCompaction=n(默認為0,表示每次進入Full GC時都進行碎片整理)
,參數作用是當CMS收集器執行過n次不整理內存碎片后,下一次進入Full GC前先進行碎片整理
- CMS收集器提供了一個參數
- 無法處理“浮動垃圾”: 在並發收集階段時,當用戶線程創建了一個對象年輕代放不下,直接晉升到老年代或者年輕代對象晉升到老年代時老年代,由於存在這種情況,因此CMS垃圾收集器必須要預留一部分空間給用戶線程(需要更大的堆空間),不能等到老年代滿了才收集(JDK5及之前是68%,JDK6之后調整為92%,可通過
-XX:CMSInitiatingOccupancyFraction_=數值_
+-XX:+UseCMSInitiatingOccupancyOnly
來設置)
異常情況
- 並發模式失敗: CMS大部分階段是與用戶線程並發執行的,如果在執行垃圾收集時用戶線程創建的對象直接往老年代分配,但是沒有足夠的內存,就會報Concurrent mode failure
- 晉升失敗: 新生代做Minor GC的時候,老年代沒有足夠的空間用來存放晉升的對象,則會報Concurrent mode failure;如果由於內存碎片問題導致無法分配,就會報晉升失敗
- 永久代空間(Java8的元空間)耗盡: 默認情況下CMS不會對永久代進行收集,一旦永久代空間耗盡,就會觸發FullGC
調優
- 硬件方面可以增加CPU核數, CMS是多線程垃圾收集器,默認啟動線程個數為(CPU核數+3)/4,CPU核數越多,對用戶線程的影響越小
- 停頓時間過長調優
- 首先需要判斷是哪個階段慢,CMS引起的停頓有:
- 年輕代Minor GC停頓
- 老年代初始、最終標記停頓
- Serial Old老年代收集停頓
- Full GC停頓
- 首先需要判斷是哪個階段慢,CMS引起的停頓有:
- 並發失敗調優
- 增大老年代的空間,增加整個堆的大小
- 提高CMS垃圾收集頻率-->>調整CMS收集的閾值
-XX:CMSInitiatingOccupancyFraction=數值
+-XX:+UseCMSInitiatingOccupancyOnly
,-XX:CMSInitiatingOccupancyFraction
調小,但也不能太小,太小了會導致過多無效的收集,浪費資源- 《Java性能權威指南》中給出的建議: 對特定的應用程序,該標志的更優值可以根據 GC 日志中 CMS 周期首次啟動失敗時的值得到。具體方法是,在垃圾回收日志中尋找並發模式失效,找到后再反向查找 CMS 周期最近的啟動記錄,然后根據日志來計算這時候的老年代空間占用值,然后設置一個比該值更小的值。
- 增加CPU回收線程個數((CPU核數+3)/4)
- 永久代調優: 如果永久代要進行垃圾回收,就會進行Full GC,CMS默認不會處理永久代中的垃圾需要通過參數
-XX:+CMSPermGenSweepingEnabled
開啟對方法區的收集,開啟后會有專門的一組線程對永久代進行垃圾回收,同時還需要開啟另一個參數-XX:+CMSClassUnloadingEnabled
,使得在垃圾收集時可以卸載不用的類。
總結
如果系統追求低延遲,那么可以選擇CMS垃圾收集器,只是STW的時間縮短了,但是整個GC的時間相對更長了;
如果系統追求高吞吐,那么可以選擇並行Parallel GC,雖然STW的時間長,但是可以保證非GC時間,整個系統的資源全部被應用線程占用。
參考資料
- 周志明《深入理解Java虛擬機》
- 不可錯過的CMS學習筆記
- 詳解CMS垃圾回收機制