Promotion Failed 的本质是老年代内存碎片化导致无法容纳大对象晋升,而非堆内存总量不足;需结合GC日志中年轻代未回收、老年代空闲但无法分配、并发模式失败三类矛盾信号定位,并通过参数调优与代码治理解决。

Promotion Failed 不是内存总量不够,而是老年代拿不出一块连续空间来放下待晋升对象。它背后是碎片化 + 大对象集中晋升的组合问题,排查要绕开“堆太小”这种表象,直击分配逻辑和对象行为。
看 GC 日志找三组矛盾信号
不能只搜“promotion failed”四个字,重点比对以下矛盾点:
- 年轻代几乎没回收:日志出现 [ParNew (promotion failed): 146112K->146112K(153600K)],说明 Eden 和 Survivor 中的对象一个都没清掉,全卡住等进老年代——但被拦下了;
- 老年代明明有空却报错:例如 OU=8.2G, OC=12G(已用 8.2G,总 12G),剩余近 4G 仍失败,证明空闲内存是碎的,拼不出几百 KB 以上连续块;
- 紧随 Concurrent Mode Failure:CMS 还没完成并发标记或清除,老年代就因碎片+新对象涌入彻底失守,被迫退化为 Serial Old 全停顿回收。
用 JVM 参数暴露晋升细节
让 JVM 主动“说话”,把隐性行为显性化:
- -XX:+PrintTenuringDistribution:观察每次 YGC 后 age=1 或 age=2 就出现 MB 级对象,说明大变量根本没在 Survivor 多待,直奔老年代;
- -XX:+PrintGCDetails -XX:+PrintHeapAtGC:对比 GC 前后老年代使用量,若单次突增 200MB+ 且伴随 Promotion Failed,基本锁定大对象集中晋升;
-
jstat -gc
1000 5 :重点关注 S0U/S1U ≈ 0(Survivor 基本没用上)和 OU 阶梯式跳涨,这是对象绕过 Survivor 直接晋升的实锤。
查代码和配置找源头
晋升失败不是凭空发生的,常见源头有三类:
- 缓存无上限:static Map 或 ConcurrentHashMap 存了大量长期存活对象,尤其任务状态、会话数据类缓存;
- DirectByteBuffer 泄漏:NIO 操作频繁但未调用 cleaner 或未触发 System.gc(),导致堆外映射残留,挤压老年代可用空间;
- 类加载器泄漏:热部署场景下旧 ClassLoader 未被回收,其加载的 Class 元数据驻留 Metaspace,间接加剧老年代碎片压力。
针对性调参与验证
优化不是盲目调大堆,而是控源头、疏路径、防碎片:
- 拦截大对象直接进老年代:-XX:PretenureSizeThreshold=1048576(1MB),让它跳过年轻代复制;
- 扩大 Survivor 容量:-XX:SurvivorRatio=4(而非默认 8),避免短命对象因 Survivor 太小而提前溢出;
- 降低碎片敏感度:CMS 场景下启用 -XX:+UseCMSCompactAtFullCollection,Full GC 时强制整理;
- 验证是否真解决:连续 3 次 Full GC 后,老年代碎片率稳定低于 12%,Promotion Failed 归零,FGC 频次下降 50% 以上。


















