CMS因浮动垃圾过多触发Concurrent Mode Failure,退化为Serial Old全停顿回收,导致STW长、卡顿;需通过GC日志、jstat、jmap等工具定位晋升异常对象并优化年轻代配置。

CMS(Concurrent Mark-Sweep)收集器在老年代并发清理时,无法处理并发阶段新产生的对象(即“浮动垃圾”),若浮动垃圾过多,会迫使 CMS 提前进入 concurrent mode failure,退化为 Serial Old 全停顿回收,表现为“回收慢”、STW 时间长、应用卡顿明显。排查关键在于确认是否因浮动垃圾堆积触发了失败,并定位其来源。
观察 GC 日志确认浮动垃圾影响
启用详细 GC 日志(如 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log),重点关注 CMS 周期中的以下信号:
- Concurrent Mode Failure:日志中出现该字样,说明 CMS 并发清理未及时完成,老年代已满,被迫启动单线程 Full GC;这是浮动垃圾问题的直接证据。
- Concurrent collection was attempted but failed:同上,表示并发周期被中断。
- 对比两次 CMS 初始标记(
Initial Mark)时的老年代使用量:若间隔很短但使用率上升极快(例如 10s 内从 65% → 95%),说明并发期间新升入老年代的对象(即浮动垃圾)速率过高。
检查对象晋升行为是否异常
浮动垃圾本质是本该在 Minor GC 中被回收、却因各种原因快速晋升到老年代的对象。需排查:
-
年轻代过小或 Survivor 区太小:导致对象躲不过几次 Minor GC 就被提前晋升(
MaxTenuringThreshold未生效)。用-XX:+PrintTenuringDistribution查看对象年龄分布,若大量对象在 age=1 或 2 就晋升,说明 Survivor 空间不足或对象存活时间判断失准。 -
大对象直接分配到老年代:检查是否有频繁创建大数组(如 byte[]、ArrayList 底层扩容)、或显式调用
System.gc()诱发不必要的晋升压力。 -
动态年龄判定阈值被突破:JVM 根据 Survivor 空间使用率动态调整晋升年龄,若 Survivor 填满快,即使对象年龄未达阈值也会晋升。日志中
Desired survivor size和实际Survivor used接近时需警惕。
监控并分析老年代对象生命周期
仅靠 GC 日志难以定位具体对象,需结合运行时工具:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
立即学习“Java免费学习笔记(深入)”;
- 用
jstat -gc <pid>持续观察OU(Old Used)和OC(Old Capacity)变化趋势,配合YGC频率,计算单位时间晋升量(如每秒晋升 MB 数)。 - 触发一次 CMS 后,用
jmap -histo:live <pid>获取当前老年代存活对象统计,重点关注数量多、总大小大的类(如缓存容器、日志对象、未关闭的连接包装类)。 - 必要时用
jmap -dump:format=b,file=heap.hprof <pid>生成堆快照,用 Eclipse MAT 分析 “Dominator Tree” 和 “Leak Suspects”,查找长期驻留、增长迅速的老年代对象引用链。
调优与规避建议
确认是浮动垃圾主导后,优先从减少晋升入手,而非强行加大老年代:
- 适当增大年轻代(
-Xmn)和 Survivor 区比例(-XX:SurvivorRatio),给对象更多在年轻代被回收的机会。 - 设置合理的
-XX:CMSInitiatingOccupancyFraction=N(如 70~75),让 CMS 更早启动并发周期,预留空间容纳浮动垃圾;注意该值过低会导致 CMS 过于频繁。 - 禁用
-XX:+UseCMSCompactAtFullCollection(默认关闭),避免 Full GC 后压缩加剧停顿;改用-XX:CMSFullGCsBeforeCompaction控制压缩频率(不推荐 CMS 环境下开启)。 - 评估升级到 G1 或 ZGC:CMS 已在 JDK 9 中被标记为废弃,JDK 14 后移除;G1 的增量回收和预测模型对浮动垃圾更友好。
浮动垃圾本身不是错误,而是 CMS 并发模型的固有约束。真正的问题往往藏在对象生命周期管理不当或代空间配置失衡中。定位清楚晋升源头,比单纯调参数更治本。

















