分代收集理论

候选人小周在面试阿里 P7 时,面试官问道:

"为什么 JVM 要分代?不分代不行吗?"

小周说:"分代可以提高 GC 效率..."面试官追问:"GC 的区域划分依据是什么?"

小周答不上来。面试官继续追问:"年轻代的对象引用了老年代,或者反过来,怎么办?"

小周彻底卡住了...

一、核心问题:分代收集理论 🔴

1.1 问题拆解

第一层:理论基础(为什么分代?)
  "分代收集的两个假说是什么?"
  考察点:弱分代假说、强分代假说

第二层:GC 区域划分(怎么划分?)
  "为什么分成年轻代和老年代?"
  考察点:对象存活率、GC 频率

第三层:跨代引用(怎么解决?)
  "年轻代和老年代之间的引用怎么处理?"
  考察点:Card Table 卡表、Remembered Set

1.2 ❌ 错误示范

候选人原话 A:"分代是因为不同对象生命周期不同。"

问题诊断:这是表面原因。真正的理论基础是弱分代假说——大多数对象朝生夕死。分代收集基于这个观察,对不同存活率的对象使用不同算法。

候选人原话 B:"跨代引用不需要特别处理。"

问题诊断:跨代引用会导致 GC Roots 的扫描范围扩大。Minor GC 时需要扫描老年代,导致性能问题。

1.3 标准回答

P5 级别:两大假说

弱分代假说(Weak Generational Hypothesis)

大多数对象朝生夕死——大部分对象在创建后很快变得不可达,只有很少一部分对象会存活很长时间。

强分代假说(Strong Generational Hypothesis)

熬过多次 GC 的对象倾向于继续存活。

基于这两个假说的设计

  • 频繁死亡的对象应该被快速回收(年轻代)
  • 长期存活的对象应该被缓慢回收(老年代)
  • 不同区域使用不同的 GC 算法

对象年龄的计数

对象每经历一次 Minor GC 并且存活(未被回收),年龄 age 增加 1。当 age 达到阈值(-XX:MaxTenuringThreshold,默认 15)时,晋升到老年代。

P6 级别:跨代引用与卡表

跨代引用的问题

graph TD
    subgraph 老年代
        O[Old 对象]
    end

    subgraph 年轻代
        Y[Young 对象]
    end

    O -->|老年代→年轻代引用| Y

当进行 Minor GC(只扫描年轻代)时,如果只扫描 GC Roots,需要扫描整个老年代来找到年轻代的引用——成本太高。

Card Table(卡表)解决方案

JVM 使用卡表(Card Table)来解决跨代引用问题:

// 卡表:将堆内存划分为固定大小的卡片(通常 512 字节)
// 每个卡片在卡表中对应一个字节
// 当一个卡片中的对象持有年轻代引用时,卡片被标记为 dirty

// Minor GC 时:
// 1. 只扫描 GC Roots
// 2. 扫描所有 dirty 卡片的跨代引用
// 3. 这些跨代引用也被作为 GC Roots

卡表的实现

堆内存 1GB → 1GB / 512B = 2M 个卡片
卡表大小 = 2M 字节 ≈ 2MB

P7 级别:Remembered Set

Remembered Set(记忆集)

G1 和其他现代 GC 使用 Remembered Set 记录跨 Region 的引用:

graph TD
    subgraph G1堆
        R1[Region 1<br/>Eden] --> RS1[Remembered Set<br/>记录指向 R1 的引用]
        R2[Region 2<br/>Old]
        R3[Region 3<br/>Survivor]
        R2 -->|跨 Region 引用| R1
        R3 -->|跨 Region 引用| R1
    end

Remembered Set 的作用是让 GC 追踪哪些 Region 中的对象引用了当前 Region,避免全堆扫描。

【面试官心理】 这道题我能问到 P7 级别,是因为分代收集理论是所有现代 GC 的基础。能说清卡表和 Remembered Set 的候选人说明他理解了 GC 优化的核心思想。

1.4 追问升级

追问:G1 的 Remembered Set 有什么性能问题?

G1 的 Remembered Set 需要维护每个 Region 的引用关系,在高引用变化率下(频繁的对象赋值)会产生大量 dirty cards,导致写屏障(Write Barrier)的开销增加。

二、生产避坑 🟡

2.1 跨代引用导致的 GC 变慢

频繁的对象赋值(跨代引用)会导致大量 dirty cards,增加 Minor GC 的成本。

2.2 参数配置

# 卡表配置
-XX:+UseCardTable    # 使用卡表(JDK 8 默认开启)
💡

面试加分点:能说出"ZGC 的着色指针(Colored Pointers)技术将 GC 信息存储在指针上而非对象头上,使得 GC 状态读取不需要访问对象头,从而避免了读屏障的性能开销",说明他理解了现代 GC 的设计哲学。

⚠️

面试陷阱:被问到"分代收集理论是谁提出的",很多人会说"不知道"。准确答案是:John McCarthy 在 1960 年的 Lisp GC 实现中首次引入了分代的概念。David Ungar 在 1984 年提出了generational garbage collection,奠定了现代分代 GC 的理论基础。