晋升失败本质是老年代内存碎片化导致无法分配连续空间,而非总空间不足;需通过GC日志矛盾信号、JVM参数暴露、调参优化及代码审查四步定位治理。

晋升失败(Promotion Failed)在 CMS 垃圾回收器中,**不是老年代总空间不够,而是拿不出一块连续空间来放下待晋升对象**。它常被误判为“堆太小”,实则是内存碎片已严重到阻断对象晋升路径。排查需绕开表象,直击碎片生成与分布逻辑。
看 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 主动“说话”,把隐性问题显性化:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
-
-XX:+PrintTenuringDistribution:观察每次 YGC 后 age=1 或 age=2 就出现 MB 级对象,说明大变量根本没在 Survivor 多待,直奔老年代; -
-XX:+PrintGCDetails -XX:+PrintHeapAtGC:对比 GC 前后老年代使用量,若单次突增 200MB+ 且伴随 Promotion Failed,基本锁定大对象集中晋升; -
jstat -gc <pid> 1000 5:重点关注S0U/S1U ≈ 0(Survivor 基本没用上)和OU 阶梯式跳涨,这是对象绕过 Survivor 直接晋升的实锤。
调参控源头、疏晋升路径、压碎片生成
CMS 本身不压缩,优化必须前置,不能等碎片满了再救:
立即学习“Java免费学习笔记(深入)”;
-
提前干预 CMS 启动时机:把
-XX:CMSInitiatingOccupancyFraction从默认 92% 降至 60~75%,让 CMS 更早介入清理,避免老年代在高度碎片化时才动手; -
延长对象在新生代滞留时间:增大
-XX:SurvivorRatio(如设为 8 或 12),扩大 Survivor 区;提高-XX:MaxTenuringThreshold(如设为 15),减少过早晋升; -
拦截大对象走捷径:虽 CMS 不支持
-XX:PretenureSizeThreshold,但可改用 G1 并配-XX:G1HeapRegionSize=1M,让大对象进 Humongous 区,避开晋升路径; -
强制碎片治理兜底:加
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=1,确保每次 Full GC 都做压缩(代价是停顿变长,但可破死循环)。
查代码确认大变量生命周期
日志和参数只是线索,最终要落到代码里:
- 搜索
new byte[...]、new ArrayList(100000)、未 close 的InputStream或流式 JSON 解析; - 检查长期持有的报表缓存、HTTP 上下文 DTO、临时构建的 Map/List 数组;
- 优先改用对象池、分片处理、软引用或及时释放,而不是靠调大堆硬扛。

















