在Java应用长时间运行之后,垃圾回收器日志里偶尔会出现 Promotion Failed 或者类似「promotion failed」的字样,这往往意味着年轻代存活对象在往老年代拷贝时,老年代没有足够的连续空间来安置它们。很多同学看到这个指标第一反应是堆不够大,于是调大 -Xmx,但过不了几天同样的问题又卷土重来。其实晋升失败指标是一把非常锋利的刀,能直接切到回收器内部碎片化这个致命死结上,只要我们学会读日志里的关键字段,就能精准定位而不是瞎扩容。

晋升失败指标在日志中到底说了什么
不同回收器打印的晋升失败信息略有差异,但核心都包含老年代当前使用量、老年代总容量,以及尝试晋升所需连续空间大小。以CMS为例,开启 -XX:+PrintGCDetails 后,一段典型日志会显示类似「CMS: promotion failed: … concurrent mode failure」的记录,前面紧跟着年轻代回收的明细。我们要关注的不是单纯的使用率百分比,而是老年代中最大可用连续块(largest free chunk)是否远小于待晋升对象总大小。
如果日志显示老年代整体使用率只有百分之六十,但却发生了晋升失败,这就说明剩下的百分之四十是碎成无数小块的空闲区域,没有任何一块能容纳下至少一个存活的大对象。这种碎片化死结和绝对容量无关,只和分配模式、回收器整理策略有关。G1虽然以 Region 化设计缓解了该问题,但在 Mixed GC 来不及回收或 Humongous 对象过多时,同样会在日志里暴露出「to-space exhausted」或晋升失败的变体。
因此读日志时建议把每次晋升失败前后的「Heap after GC」段落单独摘出来,对比老年代或各 Region 的 free 列表。可以用简单脚本过滤包含 promotion failed 的行及其前后二十行,长期统计失败发生时的平均最大连续空闲块。当这个数值稳定低于某些大对象尺寸时,就能确认碎片化是主因,而不是堆上限太低。
CMS与G1碎片化死结的成因对比
CMS采用标记清除算法,全程不进行内存整理,只有在显式触发 Full GC 或开启 -XX:+UseCMSCompactAtFullCollection 时才会压缩。这就导致短期看吞吐不错,但几个月跑下来,老年代像被老鼠啃过的奶酪,满是空洞。一旦某次年轻代回收要把一组连续的大数组晋升过去,即便总空闲够,也找不到整块地方,晋升失败指标立刻点亮。
G1把堆切成很多 Region,理论上有序复制就能整理,但它的 Humongous 对象(超过 Region 一半大小)单独放在连续 Humongous Region 里,这些区域释放后若周围已被其他存活区包围,就会形成跨 Region 的碎片。而且 G1 的 Mixed GC 依据预测模型挑选回收收益最高的 Region,如果应用突发大对象分配,回收速度跟不上,日志里就会出现晋升失败或疏散失败。两者本质都是「空闲总量充足但连续块不足」,只是触发路径不同。
从实战角度看,CMS的碎片化更不可逆,只能靠偶尔 Full GC 整理,而 Full GC 本身又会引发长暂停;G1相对可控,但需要合理设置 -XX:InitiatingHeapOccupancyPercent 与 -XX:G1HeapRegionSize,避免 Humongous 泛滥。理解这两类死结的差异,才能针对日志指标下对药。
用日志指标破除死结的实操方案
第一步是稳定采集。在启动参数中加入 -Xloggc:/var/log/app/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps,如果是JDK9以上可用统一日志 -Xlog:gc*=debug:file=gc.log。随后写一个小工具,专门抓 promotion failed 前后的老年代最大块数据,形成趋势图。当趋势向下且失败频次上升,就发出告警,而不是等 OOM。
第二步是针对性解开死结。对CMS,可以设置 -XX:CMSInitiatingOccupancyFraction=70 并配合 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0,让每次 Full GC 都压缩,虽然暂停长但能保命;更优的是迁移到 G1。对G1,则降低 Humongous 对象产生,比如把大缓存拆小,或调大 Region 到 16M、32M,并收紧 IHOP 提前启动并发标记。以下代码展示如何用 awk 提取 CMS 日志中的关键碎片指标:
awk '
/promotion failed/ { fail=1 }
fail==1 && /CMS Old Gen/ {
# 示例行: CMS Old Gen: 123456K->120000K(200000K)
if (match($0, /([0-9]+)K->([0-9]+)K(([0-9]+)K)/, a)) {
used=a[2]; total=a[3];
print "Old used:", used, "total:", total, "fragment?", (used/total > 0.5);
}
fail=0;
}
' gc_cms.log
第三步是验证。调整参数后继续观察日志,如果晋升失败指标消失,且老年代最大连续块稳定在大对象尺寸之上,说明死结已破。若仍出现,就要复查应用里是否有缓存无限增长或 DirectByteBuffer 堆积等外部因素。日志指标不是万能,但它是戳破碎片化幻觉最直接的那根针,用好了远比盲目加内存管用。
GC_logpromotion_failureheap_fragmentation修改时间:2026-08-16 09:08:29