尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

用了很多年的 CMS 垃圾收集器,终于换成了 G1,真香!2 万字详解

发布时间:2026/9/29 10:24:28

资讯中心
01
ARTICLE

用了很多年的 CMS 垃圾收集器,终于换成了 G1,真香!2 万字详解

用了很多年的 CMS 垃圾收集器,终于换成了 G1,真香!2 万字详解
如果你和曾经的我一样线上服务用了很多年 CMS 垃圾收集器一定经历过这些时刻老年代还没到阈值就突然触发 Full GC高峰期一次停顿几百毫秒甚至上秒调优参数越加越多效果却越来越不可控内存碎片严重时明明还有可用内存却频繁报分配失败。直到真正切到 G1才发现“可预测的停顿时间”这件事真的很香。本文用约 2 万字的篇幅从 CMS 的痛点讲起系统拆解 G1 的原理、回收流程、关键机制、迁移步骤与调优实战帮你少走几年弯路。1. 写在前面为什么再也回不去 CMSJVM 垃圾收集器的演进本质上是在“吞吐量”和“停顿时间”之间不断寻找更好的平衡点。早期内存小、单核为王Serial、Parallel 这类以吞吐量优先的收集器足够好用但到了互联网时代动辄几十 GB 堆、海量对象的场景下一次 Full GC 停顿几秒甚至十几秒足以让一个上游接口超时、一次大促雪崩。CMSConcurrent Mark Sweep并发标记清除收集器在 JDK 1.5 时代横空出世主打低停顿目标是让垃圾回收尽可能地与应用线程并发执行。它确实解决了“GC 全停”的燃眉之急于是大量生产系统一用就是很多年。但随着线上业务的演进CMS 的“老毛病”越来越突出并发失败会退化成单线程 Serial Old内存碎片要靠周期性的 Full GC 压缩来缓解而究竟要配置多少参数才能达到理想效果几乎是一门玄学。G1Garbage First在 JDK 7u4 商用、JDK 9 成为默认收集器它最大的亮点是“可预测的停顿时间模型”。在 CMS 时代我们求的是“尽量让停顿变短”但长短不可控到了 G1 时代我们可以直接说“我要单次停顿不超过 200ms”G1 会尽量在目标内完成回收。这种从“尽力而为”到“目标驱动”的转变正是很多人切换后感叹“真香”的根本原因。需要提前说明的是本文不是要否定 CMS 的历史价值CMS 的理念并发标记、并发清除直接影响了后面所有低停顿收集器的设计。文章的目标是帮你把 CMS 和 G1 的来龙去脉彻底搞清楚让你从“会用”升级到“理解原理、敢于调优、能够排查”。2. 基础盘JVM 垃圾收集器到底在解决什么问题在对比 CMS 和 G1 之前我们先把几个绕不开的概念铺清楚。这些概念会贯穿全文理解了它们后面看任何收集器都不会再觉得云里雾里。2.1 分代假设与堆内存划分Java 堆在经典 GC 设计中被划分为新生代和老年代其依据是“分代假设”绝大多数对象都是朝生夕死的只有少数对象能存活较长时间。因此把“死得快”的对象集中到一个区域频繁回收、低成本处理把“活得久”的对象放到另一个区域低频回收整体效率最高。新生代Young Generation又分为 Eden 区和两个 Survivor 区S0/S1也叫 From/To。新对象通常在 Eden 区分配经历一定次数 GC 后仍存活的对象会被复制到 Survivor 区再经历若干次后晋升到老年代。老年代Old Generation存放生命周期较长的对象。老年代的回收频率低但单次回收成本高。永久代 / 元空间PermGen / MetaspaceJDK 8 之前叫永久代存放类的元数据、常量池等JDK 8 起被元空间取代元空间使用本地内存不再受堆大小直接限制。新生代回收叫 Minor GC / Young GC老年代回收叫 Major GC而同时触发新生代和老年代的全局回收一般被称为 Full GC。需要注意的是CMS 和 G1 对“Full GC”的定义和触发时机并不完全一致这正是后面要重点澄清的内容。2.2 标记、清除、复制与整理垃圾收集器底层不外乎几种内存回收算法标记-清除Mark-Sweep先标记存活对象再清除未标记对象。缺点是会产生内存碎片。复制Copying把存活对象复制到另一块区域原区域整体清空。优点是天生没有碎片缺点是浪费空间适合存活率低的新生代。标记-整理Mark-Compact标记后把存活对象向一端移动整理掉碎片。优点是没有碎片缺点是移动对象成本高停顿更长。CMS 老年代采用标记-清除所以天生有碎片问题Parallel Scavenge 和 Parallel Old 采用标记-整理而 G1 在 Region 粒度上更像“复制加整理”的混合这块第 4 节会展开。2.3 STW 与并发停顿的根源所谓 STWStop The World是指 JVM 暂停所有应用线程、专心执行 GC 的阶段。这个阶段应用完全无响应是造成接口毛刺、超时的直接原因。收集器的核心设计目标之一就是在保证正确回收的前提下尽可能缩短 STW 时间。“并发Concurrent”指的是 GC 线程和应用线程同时运行“并行Parallel”指的是用多个 GC 线程同时做一件事。CMS 和 G1 都大量使用并发技术把原本必须 STW 完成的工作移到并发阶段去做从而缩短停顿。但并发也有代价会占用 CPU、会产生“浮动垃圾”还要解决并发期间对象引用变化的一致性这就是后面看到 RSet、SATB 这些机制的由来。理解了分代模型、回收算法和 STW 这三个基础再看 CMS 和 G1 的差异思路就会非常清晰。3. CMS 深度拆解曾经的低停顿王者CMS 是“以最短回收停顿时间为目标”的老年代收集器。它不像 Parallel Old 那样追求高吞吐量而是专注于减少老年代回收时的停顿。较大的互联网应用曾大量使用“ParNew 加 CMS”组合新生代用 ParNew 并行复制老年代用 CMS 并发标记清除。3.1 CMS 的回收流程CMS 的一次老年代回收分为多个阶段其中只有两个阶段需要 STW其余阶段都和应用线程并发执行初始标记Initial MarkSTW标记 GC Roots 能直接关联到的对象。虽然需要停顿但速度很快因为只标记“根直接可达”的对象。并发标记Concurrent Mark从 GC Roots 直接关联的对象开始遍历整个对象图耗时较长但与应用线程并发执行。并发预清理Concurrent Preclean处理并发标记阶段发生变化的引用关系为重新标记减少工作。重新标记RemarkSTW修正并发标记期间因应用线程运行而变动的标记记录。停顿时间比初始标记长但远小于一次完全 STW 的标记。并发清除Concurrent Sweep清除未被标记的垃圾对象释放内存与应用线程并发执行。并发重置Concurrent Reset重置 CMS 内部数据结构为下一次 GC 做准备。从流程可以看出CMS 的聪明之处在于把最耗时的“遍历对象图”和“清除垃圾”放到并发阶段。理想情况下一次 CMS 回收的 STW 只有初始标记和重新标记两段停顿控制在很短范围内。下面是一段简化的 Java 代码用来帮助理解“对象图”与 GC Rootsjavapublic class GcRootsDemo { public static Object staticRef; // 静态变量属于 GC Roots private Object instanceRef; // 实例变量 public static void main(String[] args) { GcRootsDemo demo new GcRootsDemo(); // demo 是栈上的局部变量属于 GC Roots Object a new Object(); // a 也是栈变量GC Roots 直接可达 demo.instanceRef a; // a 通过 demo.instanceRef 被引用 staticRef new Object(); // 静态变量直接持有新对象 // 方法结束后a、demo 等局部变量出栈若没有其他引用这些对象将成为垃圾 } }代码中栈上的局部变量、静态变量、JNI 引用等都是 GC Roots。CMS 的初始标记只标这些“根”并发标记再顺着引用关系把整张可达图走一遍。3.2 CMS 的三大痛点CMS 的低停顿是“有条件的好”。在实际生产环境中它有三个让人很头疼的问题。痛点一并发失败Concurrent Mode Failure由于并发标记和并发清除期间应用线程仍在运行新对象会不断被分配并晋升到老年代这些在本次回收中未被标记到的对象就是“浮动垃圾”。CMS 无法在一次回收中清掉它们只能留给下一次。因此CMS 不能等到老年代完全用完才触发回收必须预留一部分空间。这个触发比例由-XX:CMSInitiatingOccupancyFraction控制默认是 92%即老年代占用达到 92% 时启动 CMS。但如果应用分配速度过快老年代在并发回收完成前就被填满就会发生“并发失败”CMS 被迫退化为 Serial Old 单线程收集器做一次 Full GC停顿时间从几十毫秒级飙升到数秒甚至更久这在高并发场景下是灾难性的。典型的 GC 日志关键词就是concurrent mode failure一旦频繁出现就意味着 CMS 已经“顶不住”当前的对象分配速率了。痛点二内存碎片CMS 用标记-清除算法回收后老年代内存会出现大量不连续的空闲空间。当需要为大对象分配内存时即使剩余空间总量够也可能因为找不到一块连续的足够大的空间而触发 Full GC。CMS 提供了-XX:UseCMSCompactAtFullCollection和-XX:CMSFullGCsBeforeCompaction来做碎片整理但整理本身是一次 STW 的 Full GC等于“为了消灭碎片先引入一次长停顿”治标不治本。痛点三性能通胀与参数地狱CMS 的并发阶段会抢占 CPU 资源。默认的并发线程数是(CPU 核数 3) / 4核数多还好核数少的机器上 GC 和应用互相争抢 CPU反而拖慢整体吞吐量。更让人崩溃的是调优参数越来越多-XX:CMSInitiatingOccupancyFraction、-XX:UseCMSInitiatingOccupancyOnly、-XX:UseCMSCompactAtFullCollection、-XX:CMSFullGCsBeforeCompaction、-XX:ParallelGCThreads、-XX:ConcGCThreads……每个参数之间还会相互影响最终效果严重依赖机器配置和业务形态换一批机器就可能要重新调。3.3 CMS 的落幕废弃与移除CMS 的问题并非不能优化而是它的设计天花板已经到顶。JDK 9 中CMS 被标记为废弃Deprecated官方明确建议使用 G1 替代JDK 14 中 CMS 被彻底移除。对于长期停留在 JDK 8 的存量系统CMS 还能继续用但只要有升级 JDK 或重构的机会“切换到 G1”就是一条顺理成章的路线。需要强调CMS 留下的“并发标记”思想并没有消失G1、ZGC、Shenandoah 都继承了它在并发回收上的探索。我们怀念 CMS但不必留恋它的那些坑。4. G1 深度拆解可预测停顿的区域化收集器G1 是“Garbage First”的缩写直译是“垃圾优先”。它的核心思想很直接在设定的停顿时间内优先回收“垃圾最多的区域”把回收收益最大化。这个简单的目标背后是一套完全不同于传统分代收集器的堆布局和回收机制。4.1 区域化堆布局Region 是 G1 的基本单位G1 不再把堆物理上切成“一整块新生代加一整块老年代”而是把整个堆划分成大量大小相等的 Region。每个 Region 在逻辑上可以被标记为 Eden、Survivor、Old 或 Humongous大对象类型类型在运行过程中可以动态变化。换句话说原本“连续的一片新生代”这个物理概念在 G1 中变成了“一堆分散的、逻辑上属于新生代的 Region”。默认情况下G1 会把堆划分为约 2048 个 Region每个 Region 大小在 1MB 到 32MB 之间必须为 2 的幂可以通过-XX:G1HeapRegionSize显式指定。例如一个 8GB 的堆默认每个 Region 大约就是 4MB。text# 默认让 G1 自动计算 Region 大小 -Xms8g -Xmx8g -XX:UseG1GC # 显式指定每个 Region 为 8MB -XX:G1HeapRegionSize8mRegion 化带来一个巨大好处回收粒度从“整个新生代 / 整个老年代”缩小到了“一小撮 Region”。这意味着 G1 可以“挑肥拣瘦”每次只回收一部分收益最高的 Region而不是整个区域一起停。4.2 G1 的两种回收模式Young GC 与 Mixed GCG1 的日常回收分两种Young GC年轻代 GC当 Eden 区 Region 被占满时触发回收目标是所有 Eden Region 和 Survivor Region存活对象复制到新的 Survivor Region 或晋升到 Old Region。Young GC 是 STW 的但通常停顿很短。Mixed GC混合 GC当老年代 Region 的占用达到阈值由-XX:InitiatingHeapOccupancyPercent控制默认 45%时触发并发标记并发标记完成后除了回收所有年轻代 Region还会根据停顿预测模型挑选一部分“垃圾收益高”的老年代 Region 一起回收。Mixed GC 中的“Mixed”指的就是“年轻代 Region 加部分老年代 Region”混合回收。这是 G1 与传统分代收集器最大的区别老年代不必整个回收而是每次挑一部分高收益的 Region 做增量式回收。如果堆中碎片严重、晋升失败或 Mixed GC 无法及时腾出空间G1 也会退化为 Full GC。G1 的 Full GC 在 JDK 9 及以前是 Serial 单线程执行的代价很高因此调优目标之一就是尽量避免它。从 JDK 10 开始G1 的 Full GC 改为并行执行效率有所提升但“避免 Full GC”的原则仍然适用。4.3 G1 并发标记的完整流程G1 的 Mixed GC 会先经历一次并发标记周期流程与 CMS 有相似之处但细节更多初始标记Initial MarkSTW标记 GC Roots 直接可达的对象并借用一次 Young GC 完成。因为初始标记必须停下来找根G1 直接把它“搭车”在 Young GC 上省掉一次独立停顿。根区域扫描Root Region Scan并发扫描 Survivor 区中所有被老年代引用的对象。本阶段必须在下一次 Young GC 之前完成否则需要重新开始。并发标记Concurrent Mark从 GC Roots 遍历整个对象图标记存活对象。重新标记RemarkSTW利用 SATB 快照修正并发期间变动的引用完成最终标记。清理CleanupSTW 加部分并发统计各 Region 的存活对象和垃圾占比根据停顿预测模型挑选本次要回收的老年代 Region为 Mixed GC 做准备。其中部分统计工作可以并发执行。并发标记完成后G1 并不会马上开始 Mixed GC而是把“哪些老年代 Region 值得回收”记下来在后续多次 Mixed GC 中分批完成回收每次 Mixed GC 都只回收一小部分老年代 Region从而把一次大停顿“摊薄”成多次小停顿。这正是 G1 能控制停顿的核心策略。4.4 G1 的四大关键机制G1 能实现“可预测停顿”离不开下面四个核心机制机制一Region 化的堆布局。前面已经讲过Region 让回收粒度可以细化到单个区域这是“挑选高收益区域回收”的前提。Region 的类型可以在 Eden、Survivor、Old 之间动态转换因此 G1 可以在运行过程中灵活调整新生代和老年代的相对大小。机制二停顿预测模型Pause Prediction Model。G1 会根据历史回收数据估算回收每个 Region 大概需要多少时间然后按照停顿目标从高收益区域开始挑选直到累积的回收时间接近-XX:MaxGCPauseMillis为止。这个模型不是精确计算而是一个基于统计的估算它的目标不是“保证不超过”而是“尽量不超过”。机制三记忆集合 RSetRemembered Set与卡表。在传统分代收集器中跨代引用是一个麻烦问题如果只回收新生代就必须知道老年代中有哪些对象引用了新生代对象否则可能误判存活。G1 的 Region 之间同样存在跨区域引用为此 G1 为每个 Region 维护一个 RSet记录“谁引用了我”。RSet 由卡表Card Table支撑当引用关系变化时通过写屏障Write Barrier更新卡表再由并发细化线程 Refinement Thread 更新 RSet。RSet 让 G1 在回收某个 Region 时不需要扫描整个堆就能找到它的所有外部引用。机制四SATBSnapshot-At-The-Beginning与写屏障。并发标记过程中应用线程会不断修改对象引用如果处理不当就可能出现“标记时还没被引用、标记后又被释放”的对象被错误回收。G1 使用 SATB 快照算法在并发标记开始时先把当前对象图看作一个逻辑快照之后新产生的引用变化通过写屏障记录到 SATB 队列中由并发标记线程在合适时机处理。这样可以在不停顿应用的前提下保证标记的准确性。RSet 和 SATB 都用到了写屏障这也是 G1 相比 CMS 在实现上更复杂、但行为更可预测的原因。理解了这四个机制就能明白 G1 为什么能在“大堆 低延迟”场景下表现稳定Region 提供灵活的回收粒度停顿预测模型提供目标控制RSet 解决跨区域引用SATB 保证并发标记的正确性。5. CMS 与 G1 的全面对比下面从多个维度对两者做一次系统对比。很多团队在迁移时感到困惑本质上是没有把差异看清。对比维度CMSG1堆布局连续的新生代与老年代大量等大 Region逻辑分代回收算法老年代标记-清除Region 粒度复制加整理停顿控制尽力降低无明确目标通过 MaxGCPauseMillis 设目标内存碎片明显需要压缩基本没有Region 复制天然整理并发失败常见退化为 Serial Old有 IHOP 与预测模型风险更低Full GC单线程压缩代价高JDK 10 后并行可尽量避免适用堆大小中小堆更适合中大堆更有优势调优复杂度参数多且相互影响参数少主要通过停顿目标从表中可以看到G1 并不是“CMS 的微调版”而是一次架构层面的重构。它把 CMS 的经验教训吸收了进来用 Region 化和停顿预测模型解决了碎片和停顿不可控这两个最核心的痛点。6. 从 CMS 迁移到 G1 的完整步骤如果你的系统当前使用 CMS希望在保证稳定性的前提下切换到 G1建议按照以下步骤推进。6.1 第一步评估是否适合迁移G1 更适合以下场景堆内存较大例如 6GB 以上。对停顿时间有明确要求例如 P99 响应时间敏感。希望减少 Full GC 和碎片问题。JDK 版本在 8 及以上最好是 11 或 17。如果堆很小、对停顿不敏感、且现有 CMS 运行稳定可以先不动等待 JDK 升级时再切换。6.2 第二步确认 JDK 版本与收集器能力JDK 8 已经支持 G1但早期 8u 版本的 G1 在 Full GC 和并行线程上存在一些已知问题建议使用较新的 8u 版本例如 8u292 之后。JDK 11 和 JDK 17 的 G1 更成熟优先选择。6.3 第三步准备新的启动参数一个从 CMS 切到 G1 的典型参数变化如下text# 迁移前CMS -Xms8g -Xmx8g -XX:UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction75 -XX:UseCMSInitiatingOccupancyOnly # 迁移后G1 -Xms8g -Xmx8g -XX:UseG1GC -XX:MaxGCPauseMillis200 -XX:InitiatingHeapOccupancyPercent45 -XX:ParallelRefProcEnabled -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump/heap.hprof -Xlog:gc*:file/data/log/gc.log:time,uptime,level,tags:filecount10,filesize50M几点说明-XX:MaxGCPauseMillis200是 G1 的核心目标参数设得太小会让 G1 频繁回收反而增加总体开销建议从 200ms 起步再根据监控微调。-XX:InitiatingHeapOccupancyPercent45是默认值表示堆占用达到 45% 时启动并发标记。如果发现 Mixed GC 总赶不上分配速度可以调低一些。参数数量比 CMS 明显减少这就是 G1 在调优上的优势。6.4 第四步灰度验证与指标对比迁移不要一次性全量切。建议先在测试环境压测对比吞吐、延迟、GC 频率。再选择一台低峰期实例灰度切换观察 24 到 48 小时。确认稳定后逐步扩大范围直到全量。切换后持续观察 GC 日志、P99 延迟、堆水位、Full GC 次数。关键对比指标包括Young GC 次数与耗时、Mixed GC 次数与耗时、Full GC 次数、堆内存使用曲线、接口 P99 延迟、CPU 使用率。6.5 第五步回滚预案迁移前必须准备好回滚方案。保留旧参数、旧镜像、旧配置一旦出现无法快速定位的问题可以立刻切回 CMS。灰度阶段尤其要保证随时可回退。7. G1 调优实战迁移完成后日常调优同样重要。下面是几个高频调优场景。7.1 停顿时间过长如果观察到 Young GC 或 Mixed GC 停顿经常超出目标可以从以下方向排查存活对象过多导致复制成本高需要检查是否有大量长寿对象。Region 大小设置不合理Region 太大时单次回收成本高。老年代 Region 回收积压Mixed GC 每次都挑太多区域。对象分配速率过高GC 追赶不上需要从代码层减少对象创建。7.2 Mixed GC 回收跟不上如果IHOP设置过高并发标记启动太晚会导致 Mixed GC 来不及回收最终触发 Full GC。可以适当降低-XX:InitiatingHeapOccupancyPercent或者调大堆内存。7.3 Humongous 对象问题G1 中大小超过 Region 一半的对象被判定为 Humongous 对象直接分配在专门的 Humongous Region 中。如果应用中存在大量大数组、大字符串会造成 Humongous Region 频繁分配和回收影响性能。优化方向包括减少大对象、分块处理、调整 Region 大小。7.4 常见 GC 日志关键词关键词含义Pause Young (Normal)普通 Young GCPause Young (Concurrent Start)触发并发标记的 Young GCPause Remark重新标记阶段Pause Cleanup清理阶段Pause MixedMixed GCPause Full (Allocation Failure)退化为 Full GC需要重点关注to-space exhausted存活对象复制失败通常伴随 Full GC看到Pause Full或to-space exhausted时要立即排查因为它们意味着 G1 已经无法按预期完成回收。8. 常见误区与排查清单在迁移和调优过程中很多团队会踩同样的坑。下面列出几个高频误区。误区一把 MaxGCPauseMillis 设得非常小。设成 20ms 或 50ms 会让 G1 频繁回收总体停顿反而增加。建议从 200ms 起步。误区二堆大小不做调整。切到 G1 后如果堆配置仍然沿用 CMS 时代的小堆G1 的优势发挥不出来。可以适当调大堆。误区三忽略 Humongous 对象。大对象会破坏 G1 的回收节奏需要从代码层治理。误区四看到 Full GC 就调参数。应先分析 Full GC 的触发原因是 IHOP 过高、晋升失败还是 Humongous 分配过多再针对性处理。误区五不开启 GC 日志。没有 GC 日志所有调优都是盲人摸象。排查清单确认当前 JDK 版本与 G1 成熟度。检查启动参数是否合理尤其是堆大小和停顿目标。分析 GC 日志中的 Young GC、Mixed GC、Full GC 频率与耗时。检查是否存在大量 Humongous 对象。检查堆内存水位是否长期高位。检查应用层是否存在对象分配过快、缓存无界增长。对比迁移前后的 P99 延迟、吞吐、CPU 和内存指标。9. 总结从 CMS 切换到 G1不只是一次参数替换更是一次对 JVM 内存管理认知的升级。CMS 用并发标记换取低停顿但受限于标记-清除带来的碎片和并发失败G1 用 Region 化和停顿预测模型把“停顿可控”变成了一种工程上可以依赖的能力。整个迁移过程可以概括为四句话原理层面理解分代假设、回收算法、STW 和并发的本质才能理解 CMS 与 G1 的差异。痛点层面CMS 的并发失败、内存碎片、参数地狱是推动迁移的直接动力。机制层面G1 的 Region 化布局、停顿预测模型、RSet 与 SATB是它能提供可预测停顿的技术基础。实践层面迁移要评估场景、准备参数、灰度验证、持续调优并始终保留回滚预案。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。