聊聊cms GC中的concurrent mode failure

本文深入探讨了Java应用中CMS垃圾回收器遇到ConcurrentModeFailure的情况,解析了其背后的原因,包括新生代空间不足、老年代空间溢出等问题,并提供了具体的解决方案,如调整JVM参数、排查内存泄漏等。

在上一篇,耗时20多秒的young gc,你见过吗? 的结尾,给大家预告了一个有趣的case,现在开始分享一下。

 

直接上图↓

图1

从红框1中,可以看到cms full gc开始进行初始化标记了,紧接着在红框二处,开始了并发标记,说到这里,再贴个好图,帮大家回忆一下cms的几个阶段↓

图2

言归正传,图1红框3中,可以看到,有7次由于新生代空间不足,导致allocation failure引起的young gc,然后再图1红框4中,可以看到,此时jvm堆空间也已经使用了3760040K,也就是3.6G,而整个堆只有4G。

 

敲黑板,重点来了,当young gc的时候,把eden和survivor里的都还存活的对象,统一移到另一个survivor区中时,发现装不下了,就需要把部分对象,放到老年代中去,结果老年代空间也不足,这种场景呢,叫做promotion failed

 

在promotion failed的前提下,老年代恰好还正在full gc,那么就会有图1红框5中的字样提示,concurrent mode failure。

 

在图一红框6中,可以看到整个堆的使用空间从4032680K(3.8G),变成了868795K(0.8G),这是一次真实耗时3.77秒,会有STW现象的full gc ,也叫做备份gc。

 

有的同学可能会疑惑,这个full gc有点慢呀, 是的。其中一个很大的原因,就是concurrent mode failure情况下的full gc使用的并不是cms的垃圾回收器,而是使用的Serail-Old垃圾回收器,全程是单线程串行操作, 肯定比较慢。

 

为啥concurrent mode failure了,就要退化成Serail-Old呢 ?来看看R大的回答↓

 

可是,不是有一个ParallelOld回收器吗?直接用呗,为啥非要用serial-old呢?原因是这个回收器与CMS GC不兼容所以无法作为它的备份full GC使用。

 

说完了原因,再来看看解决方案,从两方面着手,首先是排查应用程序的内存泄漏问题,看看是否有频繁且大量的不必要的创建对象的操作,再就是评估下当前的系统负载和容量情况,看是否需要扩容调整。

 

如果程序代码排查没问题、容量也没问题,那就要进行jvm参数的调优了,加上-XX:UseCMSCompactAtFullCollection参数来减少内存碎片,本文中的情况是新生代较小(只有300M),因此还有很多对象晋升到老年代,于是老年代内存使用量也涨的比较快,所以可以适当通过-XX:NewSize和-XX:MaxNewSize来调整下新生代的大小。

 

如果新生代大小是合理的话,则可以调大老年代的容量,如果内存有限的话,可以适当调小XX:CMSInitiatingOccupancyFraction参数,让老年代提前进行GC,预留足够的空间来接纳新生代的晋升对象。

 

实战中,需要结合真实情况,对症下药~

 

<think>我们讨论的是CMS垃圾收集器出现Concurrent Mode Failure的原因和解决方法。 CMSConcurrent Mark-Sweep)是一种以获取最短回收停顿时间为目标的收集器,主要应用于老年代的垃圾回收。它采用“标记-清除”算法,并且在整个过程中可以与用户线程并发执行(除了初始标记和重新标记需要Stop The World)。Concurrent Mode FailureCMS在并发清理过程中遇到的一种失败情况。 原因: 1. 在CMS并发清理阶段,用户线程还在运行,同时会不断产生新的垃圾(浮动垃圾)。如果这些新产生的垃圾对象占满了老年代剩余的空间(或者超过了预留的空间),就会导致Concurrent Mode Failure。 2. 当老年代空间不足以容纳应用继续运行产生的对象时,就会触发一次Full GC(通常是Serial Old收集器),导致长时间的停顿。 解决方法: 1. 调整老年代空间大小:增加堆大小,特别是老年代的大小,可以通过参数调整。 2. 调整CMS触发的阈值:通过参数-XX:CMSInitiatingOccupancyFraction来设置老年代空间使用率触发CMS回收的阈值,预留足够的空间给并发清理期间产生的浮动垃圾。 3. 使用CMS后台收集的备用方案:通过参数-XX:+UseCMSInitiatingOccupancyOnly,让JVM严格使用设定的阈值启动CMS,避免JVM自行调整。 4. 加快CMS回收频率:如果浮动垃圾过多,可以降低触发CMS的阈值,让CMS更早启动。 5. 减少对象直接进入老年代:优化程序,减少大对象分配,避免长期存活的对象过早进入老年代(通过调整-XX:MaxTenuringThreshold等参数)。 6. 开启空间整理:使用-XX:+UseCMSCompactAtFullCollection(默认开启)在Full GC后进行碎片整理,以及-XX:CMSFullGCsBeforeCompaction设置多少次Full GC后进行碎片整理。 示例代码:虽然我们不能直接写垃圾收集器的代码,但我们可以通过模拟程序来触发Concurrent Mode Failure,然后通过参数调整来避免。 触发Concurrent Mode Failure的示例代码(通过不断创建大对象): ```java import java.util.ArrayList; import java.util.List; public class CMSTest { public static void main(String[] args) { List<byte[]> list = new ArrayList<>(); // 设置JVM参数:-Xms20M -Xmx20M -XX:NewRatio=2 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+PrintGCDetails // 这样设置堆大小为20M,老年代大约13M,当老年代占用达到70%时触发CMS,但很快会被填满 while (true) { // 每次分配1MB,由于超过Eden区大小,对象会直接进入老年代 byte[] arr = new byte[1024 * 1024]; list.add(arr); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } ``` 解决方案的JVM参数调整示例: 1. 增加堆大小:-Xms100M -Xmx100M 2. 降低CMS触发阈值:-XX:CMSInitiatingOccupancyFraction=60 3. 设置每次Full GC后都进行内存整理:-XX:CMSFullGCsBeforeCompaction=0 完整参数示例: ``` -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=60 -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 ``` 解释: - -XX:+UseConcMarkSweepGC:启用CMS收集器。 - -XX:CMSInitiatingOccupancyFraction=60:老年代使用60%时启动CMS。 - -XX:+UseCMSInitiatingOccupancyOnly:始终使用设定的阈值,禁止JVM自行调整。 - -XX:+UseCMSCompactAtFullCollection:在Full GC后进行碎片整理(默认开启,但可以显式设置)。 - -XX:CMSFullGCsBeforeCompaction=0:表示每次Full GC后都进行碎片整理。 相关问题: §§相关问题§§: 1. CMS垃圾收集器的工作流程是怎样的? 2. 什么是浮动垃圾(Floating Garbage)? 3. 为什么CMS收集器不进行内存整理(Compaction)? 4. 在哪些情况下,CMS收集器会触发Full GC?</think>### CMS 垃圾收集器出现 Concurrent Mode Failure 的原因与解决方案 #### 问题本质 **Concurrent Mode Failure** 是 CMSConcurrent Mark-Sweep)垃圾收集器在并发标记/清理阶段,老年代空间不足以容纳新晋升对象时发生的严重错误。该错误会触发 JVM 回退到 Serial Old 收集器进行 Full GC,导致长时间 STW(Stop-The-World)。 ```java // 模拟触发 Concurrent Mode Failure 的代码模式 public class CMSFailureSimulator { public static void main(String[] args) { List<byte[]> oldGenList = new ArrayList<>(); while (true) { // 持续创建大对象直接进入老年代 byte[] largeObj = new byte[10 * 1024 * 1024]; // 10MB // 同时创建短期存活对象增加GC压力 for (int i = 0; i < 100; i++) { byte[] temp = new byte[1024]; // 1KB } oldGenList.add(largeObj); System.out.println("Allocated: " + (oldGenList.size() * 10) + "MB"); } } } ``` #### 核心原因分析 1. **老年代空间不足**(最常见原因) - 对象晋升速度 > CMS 回收速度 - 大对象直接进入老年代(`-XX:PretenureSizeThreshold`) - 长期存活的年轻代对象晋升(`-XX:MaxTenuringThreshold`) 2. **内存碎片问题** ```java // 内存碎片模拟 public class MemoryFragmentation { void createFragmentation() { // 交替分配不同大小对象 for (int i = 0; i < 1000; i++) { if (i % 2 == 0) { new byte[2 * 1024]; // 2KB } else { new byte[10 * 1024]; // 10KB } } } } ``` 3. **GC 触发时机不当** - CMS 初始标记阈值设置过高(`-XX:CMSInitiatingOccupancyFraction`) - 浮动垃圾(Concurrent Mode 期间新产生的垃圾)占满空间 4. **年轻代晋升失败** - 年轻代 GC 后存活对象超过 Survivor 容量 - 对象直接晋升到老年代但空间不足 #### 解决方案与调优策略 1. **调整老年代空间** ```bash # 增加堆大小 -Xmx4g -Xms4g # 调整新生代/老年代比例 -XX:NewRatio=2 # 老年代占2/3 ``` 2. **优化 CMS 触发阈值** ```bash # 降低初始标记阈值(默认68%) -XX:CMSInitiatingOccupancyFraction=60 # 启用占用率触发策略 -XX:+UseCMSInitiatingOccupancyOnly ``` 3. **启用内存压缩** ```bash # Full GC 后压缩老年代 -XX:+UseCMSCompactAtFullCollection # 设置压缩频率 -XX:CMSFullGCsBeforeCompaction=0 # 每次FullGC后压缩 ``` 4. **优化对象分配** ```bash # 调整大对象阈值 -XX:PretenureSizeThreshold=1048576 # 1MB以上直接进老年代 # 调整晋升年龄 -XX:MaxTenuringThreshold=5 ``` 5. **增加 CMS 线程数** ```bash # 增加并发GC线程(默认=(CPU+3)/4) -XX:ConcGCThreads=4 ``` #### 诊断工具与方法 1. **GC 日志分析** ```bash # 启用详细GC日志 -Xlog:gc*:file=gc.log:time,uptime,level,tags # 典型错误日志片段 [GC (Allocation Failure) [ParNew: ...] [CMS: [CMS-concurrent-mark: ...] [CMS-concurrent-preclean: ...] [CMS-concurrent-abortable-preclean: ...] [Full GC (Allocation Failure) [CMS: ...] ``` 2. **内存分析工具** ```java // 使用jmap生成堆转储 Runtime.getRuntime().exec("jmap -dump:live,format=b,file=heapdump.hprof <pid>"); // 使用VisualVM分析 public class VisualVMAttach { public static void main(String[] args) throws Exception { String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; Runtime.getRuntime().exec("jvisualvm --openpid " + pid); } } ``` #### 高级调优技巧 1. **增量式 CMS 模式** ```bash # 适用单核/双核环境 -XX:+CMSIncrementalMode -XX:+CMSIncrementalPacing ``` 2. **避免晋升风暴** ```java // 对象池减少分配 public class ObjectPool<T> { private final Queue<T> pool = new ConcurrentLinkedQueue<>(); public T borrowObject() { T obj = pool.poll(); return obj != null ? obj : createNewObject(); } public void returnObject(T obj) { resetObject(obj); pool.offer(obj); } } ``` 3. **G1 收集器迁移** ```bash # 现代替代方案(JDK9+默认) -XX:+UseG1GC -XX:MaxGCPauseMillis=200 ``` #### 监控指标参考 | 监控项 | 健康值 | 危险阈值 | |--------|--------|----------| | 老年代占用率 | <60% | >75% | | CMS 时间占比 | <5% | >10% | | Full GC 频率 | <1次/小时 | >1次/分钟 | | STW 持续时间 | <100ms | >1s | ---
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值