CMS与G1垃圾回收器:从设计到实践的深度对比
在Java的世界里,垃圾回收(GC)是守护内存秩序的隐形引擎。随着应用规模的扩大和性能要求的提升,开发者对GC的要求也从“能用”升级为“好用”——既要低延迟保证响应速度,又要高吞吐量支撑业务负载。作为JVM发展史上的两代经典回收器,CMS(Concurrent Mark Sweep,并发标记清除)与G1(Garbage First,垃圾优先)分别代表了不同时代的GC设计哲学。本文将从设计目标、堆结构、回收策略到实际表现,全面拆解两者的核心差异,助你在技术选型时心中有数。
一、设计目标:定位决定特性
要理解CMS与G1的差异,首先需明确它们的“出生背景”与“核心使命”。
CMS:为中小内存而生,主打低停顿
CMS诞生于JDK 4(正式商用在JDK 5),当时Java应用的内存规模普遍较小(通常几十到几百MB)。它的设计目标是“在不牺牲吞吐量的前提下,尽可能减少停顿时间”,尤其适合对响应延迟敏感的场景(如Web服务器)。
其核心思路是:通过并发执行GC任务(与用户线程同时运行),将传统回收器(如Serial、Parallel)的长停顿拆解为多个短停顿,从而提升用户体验。
G1:面向大内存,可控停顿+高吞吐
随着大数据、微服务等场景的兴起,应用内存规模攀升至GB甚至TB级别。传统分代回收器(如CMS)在大内存下暴露了两大问题:
- 老年代空间过大,单次Full GC耗时剧增;
- 内存碎片化导致频繁Full GC,影响稳定性。
JDK 9正式将G1设为默认回收器,其设计目标是“在可控的停顿时间内(STW时间可预测),实现高吞吐量”,尤其适合大内存、低延迟要求的场景(如分布式计算、实时系统)。
二、堆结构:传统分代 vs 分区管理
堆内存的结构设计,直接决定了GC的实现逻辑。
CMS:经典分代,新生代与老年代独立
CMS延续了JVM传统的分代收集理论,将堆划分为固定的新生代(Young Generation)和老年代(Old Generation),两者物理隔离:
- 新生代:采用复制算法(Minor GC),分为Eden区和两个Survivor区,用于存放新创建的对象;
- 老年代:采用标记-清除算法(Major/Full GC),存放长期存活的对象。
这种结构的优势是逻辑简单、实现成熟,但缺点也很明显:
- 老年代空间一旦被占满,触发Full GC时需扫描整个老年代,停顿时间长;
- 分代边界固定,无法根据对象存活周期动态调整空间分配。
G1:分区管理,灵活的区域化策略
G1彻底颠覆了传统分代结构,采用“分区(Region)”作为基本管理单元。堆被划分为大小相等的Region(默认1MB~32MB,可通过-XX:G1HeapRegionSize
调整),每个Region可以是新生代(Eden/Survivor)、老年代(Old)或Humongous(大对象专属区域,存放超过Region 50%的对象)。
这种设计的灵活性体现在:
- 动态调整区域角色:G1会根据对象的存活情况,动态合并或拆分Region(例如,连续的新生代Region可升级为老年代);
- 聚焦回收收益:每次GC时,G1会优先回收“垃圾比例最高”的Region(即“Garbage First”),而非像CMS那样固定扫描整个老年代;
- 减少内存碎片:通过复制算法(类似新生代的复制)整理存活对象,避免CMS因标记-清除导致的碎片化问题。
三、回收策略:并发与整理的平衡艺术
回收策略是GC的核心逻辑,直接影响停顿时间和吞吐量。
CMS:并发标记+清除,老年代回收是瓶颈
CMS的回收流程分为4个阶段(以老年代回收为例):
- 初始标记(Initial Mark):STW,仅标记GC Roots直接关联的对象(停顿时间极短,约1ms~5ms);
- 并发标记(Concurrent Mark):与用户线程并发执行,遍历整个对象图,标记所有存活对象;
- 重新标记(Remark):STW,修正并发标记期间因用户线程修改引用关系导致的标记错误(停顿时间比初始标记长,但远短于Full GC);
- 并发清除(Concurrent Sweep):与用户线程并发执行,清除未标记的死亡对象,回收内存空间。
其优势是通过并发标记和清除,将老年代回收的停顿时间控制在毫秒级。但缺陷也很明显:
- 标记-清除算法的副作用:回收后内存空间不连续,可能导致大对象无法分配,触发Concurrent Mode Failure(退化为Serial Old回收,停顿时间剧增);
- 老年代回收效率低:由于不整理内存,老年代的可用空间逐渐碎片化,长期运行后回收效率下降。
G1:标记-整理+分区回收,可控停顿的核心
G1的回收策略以“混合回收(Mixed GC)”为核心,结合了标记-整理算法与分区管理:
- 全局并发标记(Global Concurrent Marking):与CMS类似,标记所有存活对象(包括新生代和老年代),但通过“记忆集(Remembered Set)”记录跨Region的引用关系,减少扫描范围;
- 增量回收(Young GC):优先回收新生代的Eden区和Survivor区(类似传统复制算法),存活对象晋升到老年代或Humongous Region;
- 混合回收(Mixed GC):在全局标记完成后,G1根据每个Region的“垃圾占比”(回收收益)排序,选择收益最高的Region(通常是老年代Region)进行回收。回收时采用复制算法(类似新生代),将存活对象移动到其他Region,同时整理内存空间,避免碎片化。
这种策略的优势在于:
- 停顿时间可控:通过
-XX:MaxGCPauseMillis
(默认200ms)参数设定目标,G1会根据历史数据预测每次回收的时间,动态调整回收的Region数量,确保总停顿不超过阈值; - 避免Full GC:通过Humongous Region专门处理大对象,并通过混合回收及时整理内存,大幅减少Full GC的触发概率。
四、停顿时间:并发的“双刃剑”
两者均采用并发机制减少停顿,但实际表现差异显著。
CMS:并发覆盖大部分阶段,但老年代回收仍有风险
CMS的并发标记和并发清除阶段几乎不占用STW时间,理论上老年代回收的停顿仅来自初始标记和重新标记(合计约10ms~20ms)。但问题出在:
- 并发标记期间的对象修改:若用户线程在并发标记时修改了大量对象的引用关系,重新标记阶段需要额外扫描这些修改(通过“卡表(Card Table)”优化,但仍可能增加停顿);
- Concurrent Mode Failure:当并发清除阶段发现老年代空间不足(因并发回收速度跟不上分配速度),会触发Serial Old回收(单线程,停顿时间可能长达秒级)。
G1:分区策略+预测模型,停顿更稳定
G1的停顿主要来自全局标记和混合回收阶段:
- 全局标记通过并发执行,仅初始标记和重新标记需要短暂STW(合计约10ms~50ms);
- 混合回收的停顿时间由
MaxGCPauseMillis
控制,G1会根据当前堆的使用情况和Region的回收收益,动态选择回收的Region数量(例如,若目标停顿是200ms,则只回收能在200ms内完成的Region)。
这种“按需回收”的策略,使得G1的停顿时间更可预测,尤其在处理大内存时,避免了CMS因内存碎片导致的偶发长停顿。
五、Full GC触发:CMS更“脆弱”,G1更“健壮”
Full GC(全堆回收)是GC性能的“红线”,频繁触发会严重影响应用响应。
CMS:碎片化与大对象分配是主因
CMS的Full GC主要由以下场景触发:
- Concurrent Mode Failure:并发清除阶段老年代空间不足,被迫触发单线程的Serial Old回收;
- 永久代/元空间不足(JDK 8前):永久代存放类元数据,若动态生成类(如反射、CGLIB)过多,可能触发Full GC;
- 大对象直接进入老年代:超过-XX:PretenureSizeThreshold的对象直接在老年代分配,若老年代空间不足且无法并发回收,触发Full GC。
由于CMS不整理内存,老年代的碎片化会随着时间推移加剧,进一步放大Full GC的风险。
G1:分区管理与预测机制降低风险
G1通过以下设计减少Full GC:
- Humongous Region:大对象直接分配到Humongous Region(而非老年代),避免老年代被大对象撑满;
- 混合回收的及时性:全局标记后,G1会尽快回收高垃圾比例的Region,避免内存耗尽;
- 记忆集优化:通过记录跨Region的引用关系,减少回收时的扫描范围,提升回收效率。
因此,G1在大内存场景下,Full GC的触发频率远低于CMS。
六、如何选择:从场景到参数
回到实际应用,如何选择CMS或G1?
场景 | 推荐回收器 | 原因 |
---|---|---|
中小内存(<4GB)、低延迟 | CMS | 分代结构简单,对小内存友好;并发回收减少停顿,适合Web服务等短请求场景。 |
大内存(>4GB)、低延迟 | G1 | 分区管理+可控停顿,避免内存碎片;混合回收高效处理大对象,适合大数据、实时系统。 |
需要高吞吐量 | Parallel | (补充)若吞吐量优先且内存不大,JVM默认的Parallel Scavenge更合适。 |
参数调优建议:
- CMS:
-XX:+UseConcMarkSweepGC
,调整-XX:CMSInitiatingOccupancyFraction
(老年代使用比例触发GC,默认92%); - G1:
-XX:+UseG1GC
,设置-XX:MaxGCPauseMillis
(目标停顿时间,默认200ms),-XX:G1HeapRegionSize
(Region大小)。
总结:GC的演进,本质是对“平衡”的追求
CMS与G1的差异,本质上是JVM对“延迟”与“吞吐量”、“固定分代”与“动态分区”、“并发效率”与“内存整理”的平衡选择。随着ZGC(Z Garbage Collector)等更先进的回收器出现(目标停顿<10ms),GC的设计仍在向“更智能、更可控”的方向演进。但对于大多数企业级应用而言,理解CMS与G1的核心差异,仍能为我们的技术决策提供坚实的基础。
下次遇到GC性能问题时,不妨先看看堆内存的大小、应用的延迟要求,再想想:是该让CMS的并发回收“轻踩油门”,还是让G1的分区策略“精准控速”?