问题

Java的堆是如何分代的?为什么分代?

答案

核心概念

JVM将堆内存划分为新生代(Young Generation)老年代(Old Generation),这种设计基于弱分代假说(Weak Generational Hypothesis):

  1. 大部分对象朝生夕死: 绝大多数对象存活时间很短
  2. 熬过多次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):

  1. 基于弱分代假说: 98%对象朝生夕死
  2. 性能优化: 高频Minor GC快速,低频Full GC慢
  3. 算法匹配: 新生代用复制算法,老年代用标记-整理
  4. 空间效率: 分而治之,避免每次全堆扫描

分代结构(How):

  • 新生代(Eden + S0 + S1): 占堆1/3,对象首次分配
  • 老年代: 占堆2/3,存储长期存活对象
  • 对象经过多次GC后从新生代晋升到老年代

关键参数:

  • -XX:NewRatio: 控制代际比例
  • -XX:SurvivorRatio: 控制新生代内部比例
  • -XX:MaxTenuringThreshold: 晋升年龄阈值