对象分代晋升规则
概述
在JVM的分代收集中,对象从新生代晋升到老年代有三个条件,满足任一即可:
- 年龄阈值晋升:对象年龄达到阈值
- 动态年龄判断:Survivor区对象累计大小超过阈值
- 大对象直接进入老年代:超过设定的大小阈值
mermaid
graph TB
A["新对象"] --> B["Eden区分配"]
B --> C{"Minor GC存活?"}
C -->|"否"| D["回收"]
C -->|"是"| E["进入Survivor区"]
E --> F{"满足晋升条件?"}
F -->|"年龄>=15"| G["晋升老年代"]
F -->|"动态年龄判断"| G
F -->|"大对象"| G
F -->|"否"| H["继续在Survivor"]
style B fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10
style E fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
style G fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10年龄阈值晋升
基本规则
对象每经历一次Minor GC存活,年龄加1。当年龄达到阈值(默认15)时晋升老年代。
为什么年龄最大是15
因为对象头的Mark Word中,记录年龄的字段只有4位,最大值为1111(二进制) = 15(十进制)。
mermaid
graph LR
subgraph "Mark Word(部分)"
Age["年龄字段<br/>4 bits"]
Max["最大值: 1111<br/>= 15"]
end
Age --> Max
style Age fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,rx:10,ry:10
style Max fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10配置参数
bash
# 设置晋升年龄阈值(有效范围: 0-15)
-XX:MaxTenuringThreshold=15对象年龄变化过程
java
public class ObjectAgeDemo {
public static void main(String[] args) {
// 创建对象,年龄为0
Object obj = new Object();
// 第1次Minor GC后存活,年龄变为1
// 第2次Minor GC后存活,年龄变为2
// ...
// 第15次Minor GC后存活,年龄变为15
// 年龄达到15,晋升到老年代
}
}动态年龄判断
常见误解
错误理解:年龄1+年龄2+...的对象大小超过Survivor区50%时,年龄大于等于最大年龄的对象进入老年代。
正确理解:从年龄小的对象开始累加大小,当累加到某个年龄N时,大小超过Survivor区的50%(TargetSurvivorRatio),则将所有年龄大于等于N的对象晋升到老年代。
源码实现
c
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
// TargetSurvivorRatio默认为50
size_t desired_survivor_size = (size_t)((((double)survivor_capacity) * TargetSurvivorRatio) / 100);
size_t total = 0;
uint age = 1;
// 从年龄1开始累加
while (age < table_size) {
total += sizes[age];
if (total > desired_survivor_size) break; // 超过50%,停止
age++;
}
// 取动态计算值和MaxTenuringThreshold的较小值
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
return result;
}计算示例
假设Survivor区大小为10MB,TargetSurvivorRatio=50%,即阈值为5MB:
plain
年龄分布:
年龄1: 2MB (累计2MB < 5MB,继续)
年龄2: 2MB (累计4MB < 5MB,继续)
年龄3: 3MB (累计7MB > 5MB,停止!)
结果: 年龄>=3的所有对象晋升老年代mermaid
graph LR
subgraph "Survivor区 10MB"
A1["年龄1<br/>2MB"]
A2["年龄2<br/>2MB"]
A3["年龄3<br/>3MB"]
A4["年龄4<br/>1MB"]
A5["年龄5<br/>0.5MB"]
end
subgraph "累计计算"
C1["累计: 2MB ✓"]
C2["累计: 4MB ✓"]
C3["累计: 7MB > 5MB ✗"]
end
A1 --> C1
A2 --> C2
A3 --> C3
style A3 fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10
style A4 fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10
style A5 fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10配置参数
bash
# Survivor区目标使用率,默认50%
-XX:TargetSurvivorRatio=50设计目的
动态年龄判断的目的是防止Survivor区溢出:
- 如果某些年龄段的对象特别多
- 等到年龄达到15才晋升可能导致Survivor区不够用
- 动态调整可以更灵活地管理内存
大对象直接进入老年代
基本规则
大对象指需要大量连续内存的对象,如长字符串或大数组。为避免在Eden区和Survivor区之间来回复制,大对象直接分配到老年代。
配置参数
bash
# 设置大对象阈值(单位: 字节)
-XX:PretenureSizeThreshold=1048576 # 1MB注意事项
PretenureSizeThreshold默认为0,即不启用该机制。大对象仍在Eden区分配,通过GC次数和动态年龄判断晋升。
仅对Serial和ParNew收集器有效,对Parallel Scavenge无效。
java
public class LargeObjectDemo {
public static void main(String[] args) {
// 设置: -XX:PretenureSizeThreshold=1000000 (约1MB)
// 小对象,在Eden区分配
byte[] small = new byte[1024]; // 1KB
// 大对象,直接进入老年代
byte[] large = new byte[2 * 1024 * 1024]; // 2MB
}
}大对象的问题
频繁创建大对象会导致:
- 老年代快速填满
- 频繁触发Full GC
- 应用性能下降
java
// 不推荐: 频繁创建大数组
public void badPractice() {
for (int i = 0; i < 1000; i++) {
byte[] data = new byte[10 * 1024 * 1024]; // 10MB
process(data);
}
}
// 推荐: 复用大数组
public void goodPractice() {
byte[] data = new byte[10 * 1024 * 1024]; // 10MB
for (int i = 0; i < 1000; i++) {
Arrays.fill(data, (byte) 0); // 清空复用
process(data);
}
}空间分配担保
机制说明
在Minor GC前,JVM会检查老年代最大可用连续空间是否大于新生代所有对象总空间:
- 如果大于:Minor GC安全,直接执行
- 如果小于:检查是否允许担保失败
mermaid
graph TB
A["Minor GC前检查"] --> B{"老年代可用空间 ><br/>新生代对象总大小?"}
B -->|"是"| C["执行Minor GC"]
B -->|"否"| D{"允许担保失败?"}
D -->|"是"| E{"老年代可用空间 ><br/>历次晋升平均大小?"}
E -->|"是"| F["尝试Minor GC"]
E -->|"否"| G["执行Full GC"]
D -->|"否"| G
F --> H{"Minor GC成功?"}
H -->|"是"| I["完成"]
H -->|"否"| G
style C fill:#50C878,stroke:#2E7D4E,stroke-width:2px,rx:10,ry:10
style G fill:#E94B3C,stroke:#B8341F,stroke-width:2px,rx:10,ry:10配置参数
bash
# JDK 6 Update 24之前需要手动开启
-XX:+HandlePromotionFailure
# JDK 6 Update 24之后默认开启,此参数已失效晋升规则总结
| 晋升条件 | 触发时机 | 配置参数 |
|---|---|---|
| 年龄阈值 | 对象年龄达到阈值 | -XX:MaxTenuringThreshold=15 |
| 动态年龄 | Survivor累计超过50% | -XX:TargetSurvivorRatio=50 |
| 大对象 | 对象大小超过阈值 | -XX:PretenureSizeThreshold=0 |
实践建议
避免过早晋升
bash
# 如果对象生命周期较短,可增大年龄阈值
-XX:MaxTenuringThreshold=15
# 增大Survivor区,容纳更多对象
-XX:SurvivorRatio=6 # Eden:S0:S1 = 6:1:1避免大对象频繁创建
java
// 使用对象池
public class ByteArrayPool {
private static final ThreadLocal<byte[]> BUFFER =
ThreadLocal.withInitial(() -> new byte[1024 * 1024]);
public static byte[] get() {
return BUFFER.get();
}
}监控晋升情况
bash
# 打印GC详情,观察晋升情况
-XX:+PrintGCDetails
-XX:+PrintTenuringDistribution输出示例:
plain
Desired survivor size 5242880 bytes, new threshold 7 (max 15)
- age 1: 1234567 bytes, 1234567 total
- age 2: 890123 bytes, 2124690 total
- age 3: 456789 bytes, 2581479 total理解对象晋升规则对于JVM调优非常重要,可以帮助我们:
- 合理设置新生代和老年代大小
- 减少不必要的Full GC
- 提升应用性能
更新: 2025-12-06 15:11:44
原文: https://www.yuque.com/u22210564/zoxfmt/xqxfh1gsiy39u5l4