JVM 四大晋升机制

对于JVM来说,有四种晋升机制。


何为对象晋升

在 Java 虚拟机(JVM)的内存管理中,绝大多数垃圾收集器(如 Serial, Parallel Scavenge, CMS, G1)都采用了分代收集理论。该理论的核心思想是根据对象的存活时间将内存划分为不同的代中,并针对不同代的特点采用不同的垃圾收集算法。

现代 JVM 通常将堆内存划分为两块:

  • 新生代 (Young Generation) :存放新创建的对象。其特点是"朝生夕死",大部分对象很快就会变得不可达。新生代是垃圾收集发生最频繁的区域,采用的收集算法是复制算法
  • 老年代 (Tenured/Old Generation):存放存活时间较长的对象。特点是对象存活率高,回收频率相对较低。

当一个对象在新生代中经历了多次垃圾收集后仍然存活,它就会被移动到老年代。这个过程就称为对象晋升


常规晋升规则

JVM有两种最常见的晋升规则

年龄阈值 (MaxTenuringThreshold)

这是最直观的晋升规则。新生代(特别是 HotSpot 虚拟机)被划分为一个 Eden 区和两个 Survivor 区(S0S1)。对象首次创建在 Eden 区。

  • 每次发生 Minor GCEden 区和其中一个 Survivor 区中存活的对象会被复制到另一个空的 Survivor 区。
  • 每经历过一次 Minor GC 且存活下来,对象的年龄 (Age) 就增加 1。
  • 当对象的年龄达到一个预定义的阈值(-XX:MaxTenuringThreshold,默认值通常为 15,CMS 下为 6)时,在下一次 GC 时,它就会被晋升到老年代。

示例:

bash 复制代码
# 设置晋升年龄阈值为 10
-XX:MaxTenuringThreshold=10

Survivor 空间不足导致提前晋升

如果在一次 Minor GC 后,存活对象的大小超过了目标 Survivor 区 的可用容量,JVM 为了确保这些对象有地方存放,会直接 将一部分(通常是所有无法放入 Survivor 的对象)晋升到老年代,而不管它们的年龄是否达到了 MaxTenuringThreshold。(通常使用空间担保机制)

这是一种保护机制,防止 Survivor 区被撑爆。


特殊的晋升机制

除了上述两条规则,JVM 还提供了两种非常重要且"特殊"的动态判定机制,它们是为了优化内存使用和性能而设计的。

动态年龄判定

核心思想

并非一定要等到对象的年龄达到 MaxTenuringThreshold 才晋升。如果 Survivor 空间中相同年龄的所有对象大小的总和大于其中一个 Survivor 空间的一半 ,那么年龄大于或等于该年龄 的对象就可以直接晋升到老年代。

  • 也就是说 : Survivor区中有一批对象,年龄分别为年龄1+年龄2+年龄n的多个对象,对象总和大小超过了Survivor区域的50%,此时就会把年龄n及以上的对象都放入老年代。

触发条件与流程

  • JVM 会在每次 Minor GC 后,遍历 Survivor 区中的对象。
  • 它会对所有对象的年龄进行排序,并从小到大地累加每个年龄的对象的总大小。
  • 当累加到某个年龄(例如年龄为 n)时,发现总和超过了 Survivor 区容量 (TargetSurvivorRatio 默认是 50%)
  • 此时,JVM 会认为从年龄 n 开始往后的对象已经足够"老"了,没必要再在 Survivor 区之间来回复制。
  • 于是,它会将所有年龄 >= n 的对象全部晋升到老年代。

参数影响:

  • -XX:TargetSurvivorRatio:指定 Survivor 空间使用率的阈值百分比(默认值 50)。动态年龄判定正是基于这个比率来计算目标大小的。

示例:

假设 Survivor 区总大小为 10MB,TargetSurvivorRatio=50

  • 计算目标阈值大小:10MB * 50% = 5MB
  • GC 后,JVM 发现:年龄1+年龄2+年龄3的对象总大小已经达到了 5.1MB(超过了 5MB),而年龄3的对象大小是 0.5MB。
  • 那么,JVM 会判定所有年龄 >= 3 的对象都可以直接晋升。

大对象直接进入老年代

核心思想

在 Serial 和 ParNew 这类使用复制算法的新生代收集器中,大对象(Large Object)的分配和回收是一个显著的性能痛点。

直接进入老年代原因

  • 分配开销:一个大对象需要寻找一块连续的、足够大的内存空间。如果将其分配在 Eden 区,可能会因为空间不足而提前触发 Minor GC,即使系统中还有很多零散的可用内存。
  • 复制开销:复制算法的核心是将存活对象在两个 Survivor 区之间来回复制。如果一个大对象在新生代中幸存,那么在下一次 Minor GC 时,复制这个大对象的成本会非常高(内存拷贝操作耗时)。
    -XX:PretenureSizeThreshold

避免大对象在 Eden 区及两个 Survivor 区之间进行复制,从而降低内存分配和垃圾收集的开销,提升性能。

工作机制

当 JVM 需要为一个新对象分配内存时,它会执行以下判断流程

空间分配担保

核心思想

在发生 Minor GC 之前 ,JVM 需要先检查一下老年代最大的可用连续空间 是否大于新生代所有对象的总大小 或者历次晋升到老年代对象的平均大小 。如果条件不满足,则会先尝试进行一次 Full GC 来腾出老年代空间,然后再进行 Minor GC。这个检查过程就是"空间分配担保"。

触发条件与流程

  1. 检查:Minor GC 前,JVM 检查老年代的剩余空间是否充足。
  2. 判断逻辑
    • 如果老年代的可用空间 大于 新生代所有对象的总大小(即最坏情况,所有对象都存活),则担保成功,直接进行 Minor GC。这样是安全的。
    • 否则,JVM 会查看 -XX:-HandlePromotionFailure 参数(JDK 6 Update 24 之后此参数已失效,规则被整合到 GC 逻辑中,但思想不变)以及历次晋升的平均大小
    • 如果老年代的可用空间 大于 历次晋升到老年代对象的平均大小,则冒险尝试进行一次 Minor GC。这是一种乐观估计,认为这次晋升的对象不会比平均大小多太多。
    • 如果小于,或者上一次冒险失败了,则担保失败 ,JVM 会先触发一次 Full GC 来清理老年代,然后再进行 Minor GC。
  3. 冒险后的处理 :如果冒险进行 Minor GC 后,发现需要晋升到老年代的对象大小确实大于 老年代的当前可用空间,此时担保彻底失败,会被迫 在 Minor GC 后紧接着进行一次 Full GC(promotion failed),这是代价最大的一种情况。

总结

机制 触发时机 核心目的 关键参数/影响因素
年龄阈值 Minor GC 后,对象年龄达标 常规晋升,保证对象经过充分熬炼 -XX:MaxTenuringThreshold
Survivor 不足 Minor GC 后,存活对象太多 保护机制,防止 Survivor 区溢出 Survivor 区大小、GC 后存活对象大小
动态年龄判定 Minor GC 后,某年龄段对象总大小超限 优化机制,减少复制,及时清理 -XX:TargetSurvivorRatio、对象年龄分布
空间分配担保 Minor GC ,检查老年代空间 风险控制机制,避免晋升失败(OOM) 老年代剩余空间、历次晋升平均大小
相关推荐
Anastasiozzzz19 分钟前
Java Lambda 揭秘:从匿名内部类到底层原理的深度解析
java·开发语言
骇客野人21 分钟前
通过脚本推送Docker镜像
java·docker·容器
铁蛋AI编程实战38 分钟前
通义千问 3.5 Turbo GGUF 量化版本地部署教程:4G 显存即可运行,数据永不泄露
java·人工智能·python
晚霞的不甘1 小时前
CANN 编译器深度解析:UB、L1 与 Global Memory 的协同调度机制
java·后端·spring·架构·音视频
马猴烧酒.1 小时前
【面试八股|JVM虚拟机】JVM虚拟机常考面试题详解
jvm·面试·职场和发展
SunnyDays10111 小时前
使用 Java 冻结 Excel 行和列:完整指南
java·冻结excel行和列
摇滚侠1 小时前
在 SpringBoot 项目中,开发工具使用 IDEA,.idea 目录下的文件需要提交吗
java·spring boot·intellij-idea
云姜.1 小时前
java多态
java·开发语言·c++
李堇1 小时前
android滚动列表VerticalRollingTextView
android·java
泉-java1 小时前
第56条:为所有导出的API元素编写文档注释 《Effective Java》
java·开发语言