Skip to content

分代/增量/分区收集算法

分代收集算法:现代 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/安全点/引用类型)

基于 VitePress 构建