垃圾回收算法
垃圾回收算法决定了 JVM 如何回收无用对象。不同算法在速度、内存碎片、停顿时间和空间浪费之间有不同取舍。
# 1. 标记-清除算法
标记-清除分两步:
- 标记所有存活对象。
- 清除未被标记的对象。
优点:
- 思路简单。
- 不需要移动对象。
缺点:
- 会产生内存碎片。
- 清除后空闲空间不连续。
- 分配大对象时可能失败。
# 2. 标记-复制算法
复制算法把内存分为两块,每次只使用其中一块。回收时把存活对象复制到另一块,然后一次性清空原区域。
优点:
- 没有内存碎片。
- 分配速度快。
- 适合存活对象少的区域。
缺点:
- 浪费一部分空间。
- 存活对象多时复制成本高。
新生代中对象大多朝生夕死,因此复制算法很适合新生代。
# 3. 标记-整理算法
标记-整理分两步:
- 标记存活对象。
- 把存活对象向内存一端移动。
优点:
- 没有内存碎片。
- 不需要像复制算法那样预留等大空间。
缺点:
- 移动对象成本较高。
- 停顿时间可能较长。
老年代对象存活率高,更适合标记-整理思想。
# 4. 分代收集
分代收集基于一个经验事实:
大多数对象生命周期很短,少数对象生命周期很长。
因此堆可以分为:
- 新生代:对象创建频繁,死亡率高。
- 老年代:对象存活时间长,死亡率低。
不同区域使用不同回收策略,提高整体效率。
# 5. 分区收集
G1、ZGC 等现代收集器更强调分区思想。
堆被划分为多个 Region 或 Page,回收时可以选择部分区域,而不是每次都处理整个堆。
分区收集有助于:
- 控制停顿时间。
- 支持大堆。
- 更灵活地安排回收优先级。
# 6. 并发与并行
并行和并发是 GC 中常见概念。
并行:
多个 GC 线程同时工作,用户线程暂停
并发:
GC 线程和用户线程同时工作
低延迟收集器通常尽量把更多工作并发化,减少 STW。
# 7. 阶段小结
GC 算法没有银弹。复制算法适合存活率低的区域,标记-整理适合存活率高的区域,分代和分区是现代 JVM 提升回收效率的重要思想。理解算法取舍,才能理解不同垃圾回收器的行为。
# 8. 三大算法图解
# 8.1 标记-清除
回收前:
[A][垃圾][B][垃圾][C][垃圾]
标记存活:
[A*][垃圾][B*][垃圾][C*][垃圾]
清除后:
[A][空][B][空][C][空]
问题是会产生碎片,后续大对象可能找不到连续空间。
# 8.2 标记-复制
From:
[A][垃圾][B][垃圾][C]
复制存活对象到 To:
To:
[A][B][C][空][空]
清空 From,交换 From/To
适合存活对象少的区域,比如新生代。
# 8.3 标记-整理
回收前:
[A][垃圾][B][垃圾][C]
整理后:
[A][B][C][空][空]
没有碎片,但移动对象和更新引用成本较高。
# 9. 算法选择和对象存活率
| 存活率 | 更适合的算法 | 原因 |
|---|---|---|
| 低 | 复制 | 只复制少量存活对象 |
| 高 | 标记-整理 | 复制大量对象成本太高 |
| 中等 | 分区/混合策略 | 根据区域收益选择 |
这就是为什么新生代和老年代通常采用不同策略。
# 10. 并发标记的难点
并发 GC 中,GC 线程和业务线程同时运行。问题是业务线程可能在标记过程中改变引用关系。
例如:
GC 正在标记
业务线程把 A.field 从 B 改成 C
如果没有额外机制,GC 可能漏标或错标对象。现代收集器会使用写屏障、读屏障、记忆集、SATB 等机制维护正确性。
# 11. Tips 快问快答
Q:为什么新生代常用复制算法?
A:因为新生代大多数对象很快死亡,存活对象少,复制成本低。
Q:标记-清除最大问题是什么?
A:内存碎片。碎片多时可能无法分配连续大对象。
Q:标记-整理为什么停顿可能长?
A:需要移动对象并更新引用,工作量较大。
Q:分代收集是算法吗?
A:更准确说是收集策略,它根据不同代的对象特点组合不同算法。