分代/增量/分区收集算法
分代收集算法:现代 GC 的基石
分代收集(Generational Collection) 不是一种全新的算法,而是利用「大多数对象朝生夕灭」的经验观察,把堆分成不同区域,对不同区域采用最合适的算法。
分代的理论基础
弱分代假说(Weak Generational Hypothesis)
大多数对象都是朝生夕灭的。
这个假说是分代收集的核心依据。通过统计数据可以验证:
典型 Java 应用的对象生命周期分布:
朝生夕灭(~80%) │████████████████│
│ ████████ │
│ │
短命对象(~15%) │██ │
│ │
中等寿命(~4%) │█ │
│ │
长寿对象(~1%) │█ │
└─────────────────┘
0 1 10 100 1000 (GC 次数)强分代假说
经历过多次 GC 仍然存活的对象,往往会继续存活。
这个假说支持了晋升(Promotion)机制:年轻代多次 GC 后仍然存活的对象,晋升到老年代,减少 Survivor 区的压力。
分代收集的堆结构
┌────────────────────────────────────────────────────────┐
│ JVM 堆 │
│ │
│ 老年代(Old Gen) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ 长期存活的对象 / 大对象 │ │ │
│ │ │ 容量:约堆的 2/3 │ │ │
│ │ │ 算法:标记-清除 或 标记-压缩 │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 年轻代(Young Gen) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ Eden │ │ S0 │ │ S1 │ │ │
│ │ │ (80%) │ │ (10%)│ │ (10%)│ │ │
│ │ │ 新对象在这 │ │MinorGC│ │MinorGC│ │ │
│ │ │ 分配 │ │复制到 │ │复制到 │ │ │
│ │ │ │ │ 这里 │ │这里 │ │ │
│ │ └──────────┘ └──────┘ └──────┘ │ │
│ │ │ │
│ │ 算法:复制算法 │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘分代收集的工作流程
对象在年轻代的流转:
1. 对象在 Eden 区分配
↓
2. Eden 区满,MinorGC
↓
3. Eden + S0 中的存活对象 ──→ S1 复制
↓
4. S0 和 S1 角色互换
↓
5. 对象年龄 +1
↓
6. 重复步骤 2-5
↓
7. 年龄超过阈值 ──→ 晋升老年代
↓
8. 老年代满 ──→ FullGC对象晋升老年代的条件
| 条件 | 说明 |
|---|---|
| 年龄达标 | 年龄 > -XX:MaxTenuringThreshold(默认 15) |
| Survivor 空间不足 | Survivor 区容纳不下存活对象 |
| 动态年龄判断 | 同年龄所有对象大小之和 > Survivor 空间的一半 |
| 大对象 | 超过 -XX:PretenureSizeThreshold 直接分配到老年代 |
跨代引用问题
分代的挑战:老年代引用年轻代
场景:老年代对象持有年轻代对象的引用
GC Roots ──→ [Old Obj] ──→ [Young Obj]
↑
如果只扫描年轻代,
这个引用会被忽略,
Young Obj 会被误判为垃圾!解决方案:记忆集(Remembered Set)
为了解决跨代引用问题,JVM 在每个 Region 中维护一个记忆集(Remembered Set,RSet),记录「谁引用了我」。
年轻代 Region:
┌────────────────────────────────────────┐
│ 对象:Young Obj │
│ │
│ RSet(记忆集): │
│ ├─ 老年代 Region A ──→ 引用了 Young Obj │
│ └─ 老年代 Region B ──→ 引用了 Young Obj │
└────────────────────────────────────────┘
MinorGC 时:
1. 不需要扫描整个堆
2. 只需扫描 RSet 中记录的老年代 Region
3. 找出被老年代引用的年轻代对象
4. 这些对象标记为存活,不被回收RSet 是 GC 效率的关键:它把「需要扫描的对象」从整个堆缩小到了 RSet 记录的对象。
增量收集算法
增量收集(Incremental Collection) 的思想是:把一次长时间的 GC 拆分成多个小步骤,每次只 GC 一小部分,从而减少单次停顿时间。
增量收集的问题
增量收集虽然减少了单次停顿,但也有明显的代价:
| 维度 | 说明 |
|---|---|
| 吞吐量降低 | GC 线程和应用线程交替执行,总体 GC 时间变长 |
| 碎片问题 | 增量式压缩比一次性压缩效果差 |
| 实现复杂 | 增量更新需要解决对象消失问题 |
增量收集的代表:增量式 CMS
早期的 CMS 回收器就使用了增量式并发——GC 线程和应用线程交替执行。
分区收集算法
分区收集(Region-based Collection) 把堆划分为多个大小相等的 Region,每个 Region 可以独立进行垃圾回收。
G1 的 Region 划分
G1 把堆划分为多个大小相等的 Region(每个 1MB~32MB):
┌──────────┬──────────┬──────────┬──────────┐
│ Eden │ Eden │ Survivor │ Old │
│ Region │ Region │ Region │ Region │
├──────────┼──────────┼──────────┼──────────┤
│ Old │ Old │ Humongous │ Free │
│ Region │ Region │ Region │ Region │
├──────────┴──────────┴──────────┴──────────┤
│ G1HeapRegionSize │
│ (可配置:1MB, 2MB, 4MB, 8MB...) │
└─────────────────────────────────────────────┘Region 类型:
| Region 类型 | 说明 |
|---|---|
| Eden Region | 年轻代,存储新分配的对象 |
| Survivor Region | 年轻代,存储经历过 MinorGC 的存活对象 |
| Old Region | 老年代,存储长期存活的对象 |
| Humongous Region | 存储大对象(超过 Region 大小 50%) |
| Free Region | 空闲区域,可分配 |
分区收集的优势
| 优势 | 说明 |
|---|---|
| 可预测停顿 | 可以设置目标停顿时间(如 200ms) |
| 无连续空间依赖 | 不需要 Eden/S0/S1 连续 |
| 灵活性 | 根据停顿时间目标动态调整回收策略 |
| 并行化 | 多个 Region 可以并行回收 |
三种算法的关系
分代收集 vs 增量收集 vs 分区收集
│
├── 分代收集 ──→ 把堆分成年轻代和老年代
│ 每代用最合适的算法
│
├── 增量收集 ──→ 把 GC 分成多个增量步骤
│ 减少单次停顿时间
│
└── 分区收集 ──→ 把堆分成多个相等 Region
每个 Region 独立管理本节小结
分代、增量、分区收集算法:
| 算法 | 核心思想 | 代表 |
|---|---|---|
| 分代收集 | 按对象年龄分代,每代用最合适的算法 | 所有现代 GC |
| 增量收集 | 把 GC 分成多个增量步骤,减少停顿 | 增量式 CMS |
| 分区收集 | 把堆分成多个相等 Region,独立管理 | G1、ZGC、Shenandoah |
分代收集是现代 GC 的标准做法,结合了复制算法(年轻代)和标记-压缩(老年代)的优点。
下一节,我们来看 GC 核心概念(System.gc/STW/安全点/引用类型)。
