晋升失败本质是老年代连续可用空间不足而非总量不足,源于cms标记-清除算法导致的内存碎片;即使已用率仅70%~85%,碎片率超25%即易触发,需通过调低cms触发阈值、增大survivor区、提高晋升年龄等优化减少碎片生成。

Java中因 -XX:CMSInitiatingOccupancyFraction 设置过高引发的晋升失败(Promotion Failure)OOM,本质是CMS垃圾收集器在老年代空间尚未充分“预警”时就已满载,导致新生代对象无法顺利晋升至老年代,最终触发 java.lang.OutOfMemoryError: Java heap space(或伴随 concurrent mode failure 日志)。
这类问题不表现为典型的内存泄漏,也不单纯是堆总大小不足,而是CMS回收节奏与应用对象晋升速率严重失配的结果,多见于JDK 7/8中使用CMS的老系统(JDK 9+ 默认G1,CMS已被弃用)。
什么是晋升失败?
在CMS模式下,GC分两个阶段协同工作:
– 新生代用ParNew回收;
– 老年代靠CMS并发标记清除。
CMS不会主动压缩老年代,只清理可回收对象。当一次Minor GC后,大量存活对象需从Eden/Survivor“晋升”到老年代,但此时老年代剩余空间 晋升失败。
此时JVM被迫退化为Full GC(Serial Old单线程STW),若Full GC仍无法腾出足够空间,就直接抛出OOM。
CMSInitiatingOccupancyFraction过高为何会诱发该问题?
该参数含义:当老年代使用率达到指定百分比时,启动CMS并发收集周期。
例如:-XX:CMSInitiatingOccupancyFraction=90 表示老年代使用率达90%才开始CMS。
问题在于:
– 设置过高(如85~95),等于把CMS“启动阈值”卡得太晚;
– 应用若存在大对象集中晋升或老年代碎片化严重(CMS不整理),90%占用可能已无连续空间容纳新晋升块;
– 等到真正需要晋升时,CMS还没来得及响应,瞬间打满,触发失败。
典型日志特征:
[GC (Allocation Failure) [ParNew: ...->..., ...] [CMS: ... (concurrent mode failure): ..., ...]
紧接着出现长时间Full GC,最后OOM。
如何识别和验证?
- 检查GC日志是否频繁出现 concurrent mode failure
- 对比老年代占用率曲线(如用GCEasy或GCViewer):看CMS是否总在>85%才启动,且每次启动后很快又逼近100%
- 观察晋升量(Promotion)是否持续高于CMS清理量(CMS GC后老年代实际下降幅度小)
- 用
jstat -gc <pid></pid>查看S0C/S1C/EC/OC和YGC/YGCT/FGC/FGCT,重点关注OC(老年代容量)与OU(已用)比值是否长期>0.85
解决方案
-
调低 CMSInitiatingOccupancyFraction:推荐设为
60~75(具体视应用晋升压力测试而定),给CMS留出响应缓冲窗口 - 搭配使用 -XX:+UseCMSCompactAtFullCollection 和 -XX:CMSFullGCsBeforeCompaction=1(仅限JDK 7/8),缓解碎片化(注意:会增加Full GC停顿)
-
减少大对象直接晋升:检查
-XX:PretenureSizeThreshold是否合理;避免短生命周期大数组、大集合反复创建 - 评估升级到G1或ZGC:G1天然避免晋升失败(基于Region+Remembered Set),ZGC更进一步消除STW,适合低延迟场景
-
必要时增大老年代占比:通过
-XX:NewRatio(如设为4→老年代占80%)或直接调大-Xmx并确保-Xms == -Xmx避免动态扩容抖动
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











