CMS (Concurrent Mark-Sweep) 垃圾回收器。它是 JDK 1.4 后期引入,并在 JDK 5 - JDK 8 期間廣泛使用的一種以低停頓時間 (Low Pause Time)?為主要目標的老年代垃圾回收器。它是 G1 出現之前解決 Full GC 長停頓問題的主要方案。
一、CMS 的設計目標與定位
-
核心目標:最小化應用停頓時間 (STW - Stop-The-World)。
-
特別關注老年代垃圾回收引起的停頓。
-
旨在避免傳統 Serial Old GC(標記-整理)導致的長時間、全局性的停頓,這對交互式應用(如 Web 服務器、GUI 應用)至關重要。
-
-
實現方式:并發 (Concurrent) 收集。
-
將垃圾回收中最耗時的標記 (Marking)?和清除 (Sweeping)?階段,盡可能與應用線程并發執行,從而大大減少需要 STW 的時間。
-
-
算法:標記-清除 (Mark-Sweep)。
-
標記:?找出所有存活的對象。
-
清除:?回收未被標記(即死亡)的對象占用的空間。
-
注意:?它不進行壓縮 (Compaction),這是它產生內存碎片的主要原因。
-
-
分代收集:?CMS 主要管理老年代 (Old Generation)。它通常與一個年輕代收集器搭配使用,如?ParNew(并行復制算法,Serial 的多線程版本)或?Serial。
-
定位:?在 G1 成熟之前,CMS 是追求低延遲老年代回收的首選方案,尤其適用于中小型堆(如 4GB - 8GB)、CPU 資源相對充足且能容忍一些內存碎片和額外 CPU 開銷的應用。
二、CMS 的工作階段 (Phases)
CMS 的垃圾回收周期(針對老年代)由多個階段組成,其中只有部分階段需要 STW:
-
初始標記 (Initial Mark - STW)
-
目標:?標記從?GC Roots?直接可達?的老年代對象(速度很快)。
-
STW 原因:?需要暫停應用線程以確保在一致性的快照下快速掃描 GC Roots(線程棧、靜態變量、JNI 引用等)。
-
優化:?這個階段通常借道一次?Young GC(Minor GC)來完成。因為 Young GC 本身就需要 STW 并掃描 GC Roots,CMS 可以“搭便車”標記那些從根集直接引用的老年代對象,避免了額外的完整根掃描停頓。所以 CMS 觸發的時機往往緊跟在一次 Young GC 之后。
-
-
并發標記 (Concurrent Mark - Concurrent)
-
目標:?從“初始標記”階段標記的直接可達對象開始,遍歷整個老年代對象圖,標記所有間接可達的存活對象。
-
并發性:?這是 CMS 減少停頓的關鍵!這個階段與應用線程同時運行。應用線程可以繼續創建新對象、更新引用關系。
-
挑戰 - 浮動垃圾 (Floating Garbage):?因為在標記過程中應用線程還在運行,可能會產生新的垃圾對象(標記階段結束后才成為垃圾)或者使已標記的對象變成垃圾。同時,應用線程修改對象引用關系可能導致“漏標”或“多標”問題。CMS 使用?增量更新 (Incremental Update)?算法來解決對象引用變化的問題(與 G1 的 SATB 不同)。
-
挑戰 - 耗時:?遍歷整個老年代對象圖,即使并發,也可能花費較長時間,特別是大堆。
-
-
重新標記 (Remark - STW)
-
目標:?修正“并發標記”階段因應用線程繼續運行而導致的標記變動,確保所有在并發標記期間存活的對象都被正確標記。處理在并發階段新晉升到老年代的對象(如果搭配 ParNew,晉升發生在 Young GC 的 STW 階段,相對容易處理)。
-
STW 原因:?為了獲得一個最終準確的存活對象視圖,需要在一個確定的點上暫停所有應用線程。
-
優化:?這個階段通常比“初始標記”長,但比“并發標記”短得多。JVM 會使用多線程并行處理來加速。可以啟用?
-XX:+CMSScavengeBeforeRemark
?參數,在重新標記前強制觸發一次 Young GC,清理掉年輕代的垃圾,減少需要掃描的年輕代對象數量(年輕代對象也可能引用老年代對象),從而縮短 STW 時間。
-
-
并發清除 (Concurrent Sweep - Concurrent)
-
目標:?回收那些在標記階段被確定為死亡對象所占用的內存空間。
-
并發性:?這個階段也與應用線程同時運行。應用線程可以繼續分配新對象(在空閑列表管理的內存區域)。
-
算法:?使用空閑列表 (Free List)?管理回收后的空間。清除器遍歷內存,將連續的死對象空間合并成空閑塊,記錄在空閑列表中,供后續分配使用。
-
結果:?回收了垃圾內存,但不進行內存整理壓縮。這導致了內存碎片問題。
-
-
并發重置 (Concurrent Reset - Concurrent)
-
目標:?為下一次 CMS 周期重置內部數據結構(如標記位圖)。
-
并發性:?與應用線程同時運行,無停頓。
-
三、CMS 的核心特性與優勢
-
低停頓時間 (Low Pause Time):?這是 CMS 最大的優勢。通過將最耗時的標記和清除工作并發執行,顯著減少了 STW 的時間(主要集中在初始標記和重新標記階段),使得老年代回收對應用響應時間的影響大大降低。
-
并發收集 (Concurrent Collection):?真正實現了垃圾回收線程與應用線程在大部分時間并行工作。
-
適用于延遲敏感型應用:?在 G1 成熟之前,是 Web 服務器、交易系統等需要快速響應的應用的首選老年代回收器。
四、CMS 的缺點與挑戰
-
內存碎片 (Memory Fragmentation):
-
根本原因:?使用標記-清除算法且不壓縮內存。長時間運行后,老年代會由許多存活對象和大小不一、分散的空閑內存塊組成。
-
后果:
-
分配失敗:?即使老年代總的空閑空間足夠,也可能因為找不到足夠大的連續空間來分配一個大對象(或晉升對象),從而觸發?Full GC (Serial Old GC)。
-
Full GC 時間長:?Serial Old GC 是單線程的標記-整理-壓縮算法,在大堆上進行壓縮會導致非常長的 STW 停頓,違背了使用 CMS 的初衷。
-
-
緩解措施:
-
-XX:+UseCMSCompactAtFullCollection
?(默認 true): 在不得不進行 Full GC 時,在 Full GC 后進行內存壓縮。 -
-XX:CMSFullGCsBeforeCompaction=n
?(默認 0): 設定在多少次不壓縮的 Full GC 后,執行一次帶壓縮的 Full GC。0
?表示每次 Full GC 都壓縮(推薦)。但這仍然意味著要經歷一次長時間的 Full GC。
-
-
-
并發模式失敗 (Concurrent Mode Failure):
-
觸發條件:
-
老年代空間不足:?在 CMS 并發周期(標記和清除)完成之前,老年代空間就被填滿了。這通常發生在:
-
老年代分配/晉升速率過快,超過 CMS 回收速度。
-
浮動垃圾過多,占用了本應回收的空間。
-
并發周期啟動太晚(
-XX:CMSInitiatingOccupancyFraction
?設置過高)。
-
-
晉升失敗 (Promotion Failed):?Young GC 后,存活對象需要晉升到老年代,但老年代沒有足夠的連續空間容納它們(即使總空間可能夠,但碎片導致)。
-
-
后果:?JVM 會立即中斷?CMS 并發周期,并觸發一次?Full GC (Serial Old GC)。這會導致一個計劃外的、長時間的 STW 停頓。
-
預防措施:
-
合理設置?
-XX:CMSInitiatingOccupancyFraction
:降低老年代空間占用閾值(如從默認 68% 設到 50% 或更低),盡早啟動 CMS 并發周期,給并發回收留出足夠的時間窗口和空間裕度。 -
必須配合?
-XX:+UseCMSInitiatingOccupancyOnly
?使用:確保 JVM?僅根據?CMSInitiatingOccupancyFraction
?的值啟動 CMS,而不是自行“自適應”調整(可能導致啟動過晚)。 -
增加堆大小或老年代比例 (
-Xmx
,?-Xms
,?-XX:NewRatio
)。 -
優化應用,減少對象創建和晉升速率、減小對象大小、避免過大的對象。
-
增加 CMS 回收線程數 (
-XX:ConcGCThreads
?/?-XX:ParallelCMSThreads
, 后者在較新版本已廢棄,推薦用?ConcGCThreads
)。
-
-
-
對 CPU 資源敏感 (CPU Sensitive):
-
原因:?并發標記和并發清除階段需要與應用線程爭搶 CPU 資源。
-
后果:
-
在 CPU 資源緊張(如 CPU 核數少、負載高)的情況下,并發回收線程會拖慢應用線程的執行速度,導致應用吞吐量下降。
-
并發階段本身可能因為 CPU 爭搶而執行得更慢,增加了并發模式失敗的風險。
-
-
建議:?CMS 更適合?CPU 資源相對富余(核數較多或負載不高)的機器。
-
-
浮動垃圾 (Floating Garbage):?如前所述,并發過程中產生的垃圾只能在下一次 GC 回收。需要預留足夠空間容納這些浮動垃圾。
-
元空間/永久代觸發 Full GC:?CMS 不管理元空間 (Metaspace, JDK 8+) 或永久代 (PermGen, JDK 7-)。如果元空間/永久代空間不足,會觸發 Full GC。
-
JDK 9+ 中已棄用 (Deprecated),JDK 14+ 中已移除 (Removed):
-
棄用原因:?G1 作為更現代、設計更優的回收器(同樣追求低延遲,且解決了碎片問題)已成為默認選擇。CMS 的維護成本高,且其架構難以適應更新的 Java 特性和硬件發展(如非常大的堆)。
-
后果:?在新版本 JDK (>=14) 中無法再使用 CMS。仍在使用的應用應盡快遷移到 G1 或其他回收器(如 ZGC, Shenandoah)。
-
五、關鍵配置參數
-
啟用 CMS:
-
-XX:+UseConcMarkSweepGC
?(JDK 8 及之前)
-
-
設置年輕代收集器 (通常自動選擇):
-
搭配 ParNew:
-XX:+UseParNewGC
?(通常啟用 CMS 會自動啟用)
-
-
觸發 CMS 的堆占用閾值 (最重要!):
-
-XX:CMSInitiatingOccupancyFraction=<percent>
?(e.g., 70): 當老年代空間占用達到此百分比時,啟動 CMS 并發收集周期。建議設低一些(如 50-70)以預防并發模式失敗。
-
-
強制使用閾值觸發 (必須配!):
-
-XX:+UseCMSInitiatingOccupancyOnly
:?強制?JVM 只使用?CMSInitiatingOccupancyFraction
?的值作為觸發條件,禁用 JVM 的自適應調整。
-
-
重新標記前進行 Young GC:
-
-XX:+CMSScavengeBeforeRemark
: 在重新標記階段前強制觸發一次 Young GC,減少需要掃描的年輕代對象,有效縮短重新標記 STW 時間(強烈推薦啟用)。
-
-
CMS 線程數:
-
-XX:ConcGCThreads=<n>
?/?-XX:ParallelCMSThreads=<n>
?(后者較舊): 設置并發階段(標記、清除)使用的線程數。默認為?(ParallelGCThreads + 3) / 4
。可根據 CPU 核數調整。
-
-
Full GC 后壓縮:
-
-XX:+UseCMSCompactAtFullCollection
?(默認 true): Full GC 后進行壓縮。 -
-XX:CMSFullGCsBeforeCompaction=<n>
?(默認 0): 執行 n 次不壓縮的 Full GC 后,執行一次帶壓縮的 Full GC。0
?表示每次都壓縮。
-
六、何時使用(或曾經使用)CMS?
-
歷史場景 (JDK 8 及之前):
-
應用對老年代回收停頓時間非常敏感。
-
應用運行在中小型堆(如 4GB - 8GB)上。
-
機器有富余的 CPU 資源(核數較多,負載不高)。
-
應用能夠容忍一定程度的內存碎片或通過配置降低了 Full GC 風險。
-
需要避免 Serial Old GC 的長停頓。
-
-
當前狀態:
-
JDK 9+:已棄用 (Deprecated)。
-
JDK 14+:已移除 (Removed)。
-
強烈建議所有仍在使用 CMS 的應用遷移到?G1(目前默認且成熟)或探索新一代超低延遲回收器?ZGC?/?Shenandoah(尤其超大堆和極致低延遲需求)。
-
七、總結
CMS 垃圾回收器是 JVM 垃圾回收發展史上一個重要的里程碑,它率先通過并發標記清除的方式顯著降低了老年代回收的停頓時間,滿足了當時眾多對延遲敏感型 Java 應用的需求。其核心價值在于并發性帶來的低 STW 停頓。
然而,CMS 的固有缺陷也非常明顯:
-
標記-清除算法導致內存碎片,?最終可能引發長時間的 Serial Old Full GC。
-
對并發模式失敗 (Concurrent Mode Failure) 非常敏感,?需要精細調優(尤其是?
CMSInitiatingOccupancyFraction
?和?UseCMSInitiatingOccupancyOnly
)。 -
并發階段占用 CPU 資源,影響吞吐量。
-
無法管理元空間/永久代。
-
已被現代 JDK (>=14) 徹底移除。