问题
Java的堆是如何分代的?为什么分代?
答案
核心概念
JVM将堆内存划分为新生代(Young Generation)和老年代(Old Generation),这种设计基于弱分代假说(Weak Generational Hypothesis):
- 大部分对象朝生夕死: 绝大多数对象存活时间很短
- 熬过多次GC的对象难以消亡: 存活时间长的对象会继续存活很长时间
堆的分代结构
1. 整体划分
┌─────────────────────────────────────────────────────────┐
│ Java堆(Heap) │
├─────────────────────────────────────┬───────────────────┤
│ 新生代(Young Generation) │ 老年代(Old Gen) │
│ (约1/3堆空间) │ (约2/3堆空间) │
├──────────┬──────────┬───────────────┤ │
│ Eden区 │Survivor0 │ Survivor1 │ │
│ (8/10) │ (1/10) │ (1/10) │ │
└──────────┴──────────┴───────────────┴───────────────────┘
2. 新生代(Young Generation)
组成:
- Eden区: 新对象分配区域(占新生代80%)
- Survivor区: 两个大小相同的区域(From和To,各占10%)
特点:
- 对象首次分配在Eden区
- Minor GC频繁,回收效率高
- 使用复制算法(Copying)
默认比例:
-XX:SurvivorRatio=8 # Eden:Survivor0:Survivor1 = 8:1:1
3. 老年代(Old Generation)
特点:
- 存储长期存活的对象
- 空间大(约占堆的2/3)
- Full GC频率低,但耗时长
- 使用标记-清除或标记-整理算法
代际比例:
-XX:NewRatio=2 # 老年代:新生代 = 2:1
对象的分代流转
1. 正常流转过程
// 对象生命周期示例
public class GenerationFlow {
public static void main(String[] args) {
// 步骤1: 对象在Eden区创建
User user = new User("Alice");
// 步骤2: 经历Minor GC后,存活对象进入Survivor区
// age = 1
// 步骤3: 再次经历Minor GC,在Survivor区间来回复制
// From Survivor -> To Survivor (age++)
// 步骤4: 年龄达到阈值(默认15)后,晋升到老年代
// 或Survivor空间不足时,直接晋升老年代
// 步骤5: 在老年代中长期存活,直到Full GC
}
}
2. 详细流转图
[新对象] → Eden区
↓ (Minor GC)
存活对象 → Survivor0 (age=1)
↓ (Minor GC)
Survivor1 (age=2)
↓ (Minor GC)
Survivor0 (age=3)
↓ (反复在S0/S1间复制)
age >= 15 或 特殊情况
↓
老年代 (长期存活)
↓
Full GC 回收
3. 年龄阈值
-XX:MaxTenuringThreshold=15 # 晋升年龄阈值(默认15,最大15)
-XX:TargetSurvivorRatio=50 # Survivor目标使用率
// 对象头中存储GC年龄(4bit,最大15)
// 每经历一次Minor GC,age++
特殊情况
1. 大对象直接进入老年代
// 大对象直接分配到老年代
byte[] largeArray = new byte[5 * 1024 * 1024]; // 5MB
// 参数控制
// -XX:PretenureSizeThreshold=3145728 // 大于3MB直接进老年代(仅Serial和ParNew有效)
2. 动态年龄判定
// 如果Survivor中相同年龄对象的总大小 > Survivor空间的一半
// 则年龄 >= 该年龄的对象直接进入老年代,无需等到MaxTenuringThreshold
// 示例:
// Survivor空间: 10MB
// age=3的对象: 3MB
// age=4的对象: 4MB
// 如果 3MB + 4MB > 5MB,则age>=3的对象都晋升老年代
3. 空间分配担保
// Minor GC前检查:
// 老年代最大可用连续空间 > 新生代所有对象总大小
// 如果担保失败,触发Full GC
// 参数(JDK 6 Update 24后默认开启)
// -XX:+HandlePromotionFailure
为什么要分代?
1. 理论基础: 弱分代假说
IBM研究数据:
- 98%的对象在创建后很快死亡(朝生夕死)
- 只有2%的对象能存活较长时间
// 短命对象示例(98%)
public void process() {
String temp = "temporary"; // 方法结束即可回收
int count = 0;
List<String> localList = new ArrayList<>(); // 局部对象
}
// 长命对象示例(2%)
public class Service {
private static Cache cache = new Cache(); // 静态对象,长期存活
private Connection connection; // 连接对象,生命周期长
}
2. 性能优化: 分而治之
新生代优化:
- Minor GC频繁但快速(毫秒级)
- 使用复制算法,效率高
- 每次回收大量对象(98%)
老年代优化:
- Full GC少但慢(秒级)
- 对象存活率高(不适合复制算法)
- 使用标记-清除/整理算法
# 性能对比(典型应用)
Minor GC: 10-50ms, 每分钟多次
Full GC: 1-10s, 每小时数次
# 如果不分代,每次都对整个堆进行GC,耗时会非常长
3. 算法匹配: 不同代用不同算法
| 分代 | 对象特点 | GC算法 | 原因 |
|---|---|---|---|
| 新生代 | 存活率低(2%) | 复制算法 | 少量存活对象复制效率高 |
| 老年代 | 存活率高(95%+) | 标记-清除/整理 | 大量存活对象不适合复制 |
4. 空间效率: 按需分配
// 新生代空间小,回收快
// -Xmn512m (新生代512MB,够用即可)
// 老年代空间大,长期存活对象多
// 如果不分代,整个堆都需要按老年代大小配置,造成浪费
分代参数配置
# 堆总大小
-Xms4g -Xmx4g
# 新生代大小(方式1: 直接指定)
-Xmn1g
# 新生代大小(方式2: 通过比例)
-XX:NewRatio=2 # 老年代:新生代 = 2:1, 则新生代占堆的1/3
# 新生代内部比例
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
# 晋升阈值
-XX:MaxTenuringThreshold=15
# 大对象阈值
-XX:PretenureSizeThreshold=3145728 # 3MB
实际案例
// 案例: 高吞吐应用的分代配置
// 4GB堆内存
-Xms4g -Xmx4g
-XX:NewRatio=2 // 新生代1.33GB, 老年代2.67GB
-XX:SurvivorRatio=8 // Eden 1.06GB, S0/S1各68MB
-XX:MaxTenuringThreshold=15
// 对象分布:
// - 请求处理中的临时对象: Eden区(占98%)
// - 缓存、连接池等长期对象: 老年代(占2%)
// 这样配置能最大化GC效率
监控分代GC
# 查看GC日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps
# 输出示例
[GC (Allocation Failure) [PSYoungGen: 512000K->5120K(614400K)] 512000K->5128K(2010112K), 0.0123456 secs]
# PSYoungGen: 新生代GC(Minor GC)
# 512000K->5120K: 新生代从512MB降到5MB
# 耗时12ms
[Full GC (Ergonomics) [PSYoungGen: 5120K->0K(614400K)] [ParOldGen: 1048576K->1048500K(1395712K)] 1053696K->1048500K(2010112K), 3.5678 secs]
# Full GC: 包含新生代和老年代
# 耗时3.5秒
G1等新型收集器
// G1收集器仍使用分代思想,但实现不同
// 将堆划分为多个Region,逻辑上仍分新生代和老年代
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 // 期望最大暂停时间
-XX:G1HeapRegionSize=16m // 每个Region大小
面试总结
分代原因(Why):
- 基于弱分代假说: 98%对象朝生夕死
- 性能优化: 高频Minor GC快速,低频Full GC慢
- 算法匹配: 新生代用复制算法,老年代用标记-整理
- 空间效率: 分而治之,避免每次全堆扫描
分代结构(How):
- 新生代(Eden + S0 + S1): 占堆1/3,对象首次分配
- 老年代: 占堆2/3,存储长期存活对象
- 对象经过多次GC后从新生代晋升到老年代
关键参数:
-XX:NewRatio: 控制代际比例-XX:SurvivorRatio: 控制新生代内部比例-XX:MaxTenuringThreshold: 晋升年龄阈值