对象晋升老年代由JVM动态计算阈值决定,非固定年龄;满足年龄≥动态阈值或≥MaxTenuringThreshold(默认15)任一条件即晋升,取二者较小值;动态阈值基于Survivor区各年龄对象累计空间是否超TargetSurvivorRatio(默认50%)实时确定。

对象晋升老年代不是靠“活够几轮”来决定的,而是 JVM 在每次 Minor GC 后,根据 Survivor 区的实际空间占用情况实时算出来的结果。它既不是固定数,也不看业务逻辑,只看内存压力。
年龄计数器只是记录,不是晋升指令
对象每在 Survivor 区熬过一次 Minor GC,年龄就 +1,但这只是个标记。真正触发晋升的,是两个并行条件中任意一个满足:
- 年龄 ≥ 动态算出的阈值(比如 age ≥ 2)
- 年龄 ≥ -XX:MaxTenuringThreshold 设置的上限(默认 15)
最终取两者中更小的那个作为本次晋升门槛。所以即使你设了 max=15,只要动态阈值算出来是 2,age2 及以上的对象就全走。
动态年龄判定怎么算?看空间,不看时间
JVM 每次 Minor GC 后,从 age=1 开始累加 Survivor 中各年龄档对象所占大小,一旦累计值首次超过 Survivor 总容量 × TargetSurvivorRatio(默认 50%),当前这个 age 就成为 new threshold。
立即学习“Java免费学习笔记(深入)”;
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- Survivor 总空间 1MB,age1 占 600KB → 累计超 500KB → new threshold = 1
- age1 占 300KB,age2 占 250KB → 累计 550KB > 500KB → new threshold = 2
- 所有 age 累计 ≤ 50% → 才退回去用 MaxTenuringThreshold
这个过程不关心对象“该不该活这么久”,只防止 Survivor 被某一年龄段对象撑满,导致下一轮 GC 没地方放新存活对象。
哪些配置真起作用?别调错地方
单改 -XX:MaxTenuringThreshold 几乎没用,它只是兜底上限。真正影响动态阈值的是这三个联动参数:
- -XX:SurvivorRatio=6~8:让每个 Survivor 占年轻代 1/8~1/10,留出缓冲余量,避免 age1 就爆掉
- -XX:TargetSurvivorRatio=50(默认):判定触发线,仅在 -XX:+UseAdaptiveSizePolicy 开启时生效(默认开)
- -XX:+PrintGCDetails:必须打开,才能在日志里看到 “new threshold X (max Y)” 这一行,确认实际值
如果日志里长期出现 new threshold 1 或 2,说明 Survivor 太小,或某类对象生命周期高度一致(比如批量请求生成的 DTO),不是年龄设低了,而是空间配得不合理。
晋升本质是空间保底策略,不是生命周期判断
动态年龄判定的目标从来不是“识别长期存活对象”,而是保证 Survivor 区能持续运转。哪怕一个对象只活了两次 GC,只要它和更小年龄的对象加起来占了 Survivor 一半以上,就得立刻挪走——否则下一轮 GC 时,新存活对象可能无处可放,被迫直接进老年代,甚至触发 Full GC。
它响应的是内存布局变化,不是业务语义。优化方向不是“拦住晋升”,而是让 Survivor 容量、对象分布、GC 频率之间更匹配。

















