对象晋升失败本质是老年代空间碎片化,表现为年轻代零回收、老年代有空闲但无法分配、伴随Concurrent Mode Failure或to-space exhausted;需结合日志、-XX:+PrintTenuringDistribution及jstat交叉分析,根因多为无上限缓存、DirectByteBuffer泄漏或类加载器泄漏。

对象晋升失败(Promotion Failed)在 GC 日志中不是一次普通的 GC 异常,而是年轻代对象无法进入老年代的明确信号——它直接暴露老年代已丧失连续分配能力。分析关键不在“有没有失败”,而在“为什么失败”“谁导致失败”“是否反复发生”。
看日志里三处矛盾性数字
晋升失败的本质是“老年代有空间,但没连续块”,所以要抓日志中自相矛盾的三组数据:
-
年轻代几乎零回收:日志出现类似
[ParNew (promotion failed): 146112K->146112K(153600K)],说明 Eden 和 Survivor 中的对象一个都没清掉,全卡住等晋升,却被拦下; -
老年代明明有空却报错:比如
OU=8.2G, OC=12G(已用 8.2G,总容量 12G),剩余近 4G 仍失败,证明空闲内存是碎的; - 紧随 Concurrent Mode Failure 或 to-space exhausted:CMS 下出现前者,G1 下出现后者,都是碎片化坐实的铁证,不是配置小,是结构坏。
定位晋升对象的大小和年龄分布
仅知道失败不够,得知道“是谁在撞门”:
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
- 开启
-XX:+PrintTenuringDistribution,查每次 Minor GC 后各 age 区间对象大小。若age=1就出现数百 KB 甚至 MB 级对象,大概率是大数组、大缓存、流式解析结果等直接溢出; - 从
[PSYoungGen: A->B] C->D计算单次晋升量:D − C即为老年代增长量。若该值达 300MB+ 且伴随 promotion failed,基本可断定是批量大对象集中冲击; - 观察
Desired survivor size和动态阈值(如new threshold (max 6) = 1):若阈值被压到 1~2,说明 Survivor 存不下,对象被迫“未满月就送养老院”,加剧老年代碎片。
交叉验证:用 jstat 看实时晋升行为
GC 日志是快照,jstat 是录像。运行 jstat -gc <pid> 1000 5,重点关注:
立即学习“Java免费学习笔记(深入)”;
-
S0U/S1U是否长期接近S0C/S1C(Survivor 使用率 >90%),说明 Survivor 常年吃紧; -
EU(Eden 使用量)每次 GC 后是否陡降,但OU(老年代使用量)同步跳涨 —— 这是对象“假死亡、真搬家”的典型表现; -
YGCT(Young GC 总耗时)占比持续升高,而FGCT也上升,说明年轻代已失去缓冲作用,压力直灌老年代。
锁定根因:三类高频源头优先排查
晋升失败不会凭空发生,背后总有稳定喂养源:
- 无上限缓存:static Map / ConcurrentHashMap 存放任务状态、用户会话、计算中间结果,未设 size 控制或 LRU 淘汰;
-
DirectByteBuffer 泄漏:NIO 文件读写、网络收发后未显式调用
cleaner或依赖 System.gc(),堆外映射长期驻留,间接挤压老年代可用空间; - 类加载器泄漏:热部署、OSGi、Spring Boot DevTools 场景下,旧 ClassLoader 被强引用持有,其加载的 Class 元数据无法卸载,Metaspace 持续膨胀,最终触发 Full GC 并加剧老年代碎片。

















