对于JVM来说,有四种晋升机制。
何为对象晋升
在 Java 虚拟机(JVM)的内存管理中,绝大多数垃圾收集器(如 Serial, Parallel Scavenge, CMS, G1)都采用了分代收集理论。该理论的核心思想是根据对象的存活时间将内存划分为不同的代中,并针对不同代的特点采用不同的垃圾收集算法。
现代 JVM 通常将堆内存划分为两块:
- 新生代 (Young Generation):存放新创建的对象。其特点是“朝生夕死”,大部分对象很快就会变得不可达。新生代是垃圾收集发生最频繁的区域,采用的收集算法是复制算法。
- 老年代 (Tenured/Old Generation):存放存活时间较长的对象。特点是对象存活率高,回收频率相对较低。
当一个对象在新生代中经历了多次垃圾收集后仍然存活,它就会被移动到老年代。这个过程就称为对象晋升。
常规晋升规则
JVM有两种最常见的晋升规则
年龄阈值 (MaxTenuringThreshold)
这是最直观的晋升规则。新生代(特别是 HotSpot 虚拟机)被划分为一个 Eden
区和两个 Survivor
区(S0
和 S1
)。对象首次创建在 Eden
区。
- 每次发生 Minor GC,
Eden
区和其中一个Survivor
区中存活的对象会被复制到另一个空的Survivor
区。 - 每经历过一次 Minor GC 且存活下来,对象的年龄 (Age) 就增加 1。
- 当对象的年龄达到一个预定义的阈值(-XX:MaxTenuringThreshold,默认值通常为 15,CMS 下为 6)时,在下一次 GC 时,它就会被晋升到老年代。
示例:
# 设置晋升年龄阈值为 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。这个检查过程就是“空间分配担保”。
触发条件与流程
- 检查:Minor GC 前,JVM 检查老年代的剩余空间是否充足。
- 判断逻辑:
- 如果老年代的可用空间 大于 新生代所有对象的总大小(即最坏情况,所有对象都存活),则担保成功,直接进行 Minor GC。这样是安全的。
- 否则,JVM 会查看
-XX:-HandlePromotionFailure
参数(JDK 6 Update 24 之后此参数已失效,规则被整合到 GC 逻辑中,但思想不变)以及历次晋升的平均大小。 - 如果老年代的可用空间 大于 历次晋升到老年代对象的平均大小,则冒险尝试进行一次 Minor GC。这是一种乐观估计,认为这次晋升的对象不会比平均大小多太多。
- 如果小于,或者上一次冒险失败了,则担保失败,JVM 会先触发一次 Full GC 来清理老年代,然后再进行 Minor GC。
- 冒险后的处理:如果冒险进行 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) | 老年代剩余空间、历次晋升平均大小 |