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

晋升失败(Promotion Failed)本质不是老年代“总空间不足”,而是**连续可用空间不足**——即内存碎片问题。CMS 使用标记-清除算法,不移动对象,长期运行后老年代中空闲内存被切割成大量小块,即使总剩余空间充足,也无法容纳一个需晋升的较大对象(比如几十KB以上的缓存对象、临时集合等),从而触发失败。
为什么碎片会让晋升卡住
Minor GC 后,一批存活对象要从 Survivor 区晋升到老年代。JVM 会检查老年代是否有“一块连续空间”能放下最大那个待晋升对象。如果所有空闲块都小于该对象大小,哪怕加起来有几MB,也会直接报 promotion failed。
- 典型表现:GC 日志中出现 ParNew (promotion failed),紧接着是 CMS Full GC 或 Serial Old 回收
- 常见诱因:频繁分配生命周期中等的对象(如 HTTP 请求上下文、DTO)、Survivor 区过小导致过早晋升、MaxTenuringThreshold 设置偏低
- 监控佐证:老年代已用率可能仅 70%~85%,但碎片率(可通过
jstat -gc结合历史日志估算,或使用 GCEasy 等工具分析)持续高于 25%
关键影响变量:哪些配置直接影响连续性
老年代能否凑出一块连续空间,不只看当前容量,更取决于 CMS 的回收节奏、对象分布和晋升行为本身:
- -XX:CMSInitiatingOccupancyFraction:设得过高(如默认 92%),CMS 启动太晚,老年代已高度碎片化且无暇整理;建议压到 60~75%,让 CMS 更早介入清理
- -XX:MaxTenuringThreshold:值过小(如设为 1 或 2),大量对象“一活过一次 Minor GC 就晋升”,加剧老年代短命对象堆积与碎片生成
- -XX:SurvivorRatio:Survivor 区太小(如设为 2),对象很快溢出,被迫提前晋升;适当调大(如 8 或 12)可延长对象在新生代的滞留时间
-
未启用压缩类卸载:若关闭
-XX:+CMSClassUnloadingEnabled,元数据持续累积,间接挤压老年代可用连续空间
为什么 CMS 自己无法解决碎片
CMS 的设计目标是低延迟,为此放弃压缩步骤。它的并发清除阶段只“抹掉”标记为垃圾的对象,留下空洞,不挪动任何存活对象。所以它能快速清出空间,但无法拼出连续块。
- 晋升失败发生时,JVM 别无选择:只能暂停所有线程(STW),切换为 Serial Old 收集器,执行带压缩的 Full GC —— 这个过程要移动对象、整理内存,耗时往往是普通 CMS 的 5~10 倍
- 注意:
-XX:+UseCMSCompactAtFullCollection只在退化后的 Full GC 中起作用,不能预防晋升失败;它只是让“失败之后的补救更干净”,不能避免失败本身
真正有效的缓解方向
与其等失败后再压缩,不如从源头减少碎片生成,并提升 CMS 对碎片的应对能力:
- 降低晋升频率:增大年轻代(
-Xmn)、调高-XX:MaxTenuringThreshold(如 12~15),让对象尽量在新生代就自然死亡 - 加快老年代清理节奏:固定触发阈值(
-XX:+UseCMSInitiatingOccupancyOnly)+ 调低-XX:CMSInitiatingOccupancyFraction=65 - 控制大对象直入老年代:避免
-XX:PretenureSizeThreshold设置过低;对已知的大缓存对象,考虑对象池复用或堆外存储 - 长期方案:评估迁移到 G1(支持并发整理)或 ZGC(无碎片压力),尤其当 CMS 晋升失败日均超过 2 次时











