GC 算法大揭秘:标记清除、复制、标记整理、分代回收分别是啥?

垃圾回收器是 JVM 的核心“管家”,但它并不神秘,而是基于几种经典算法演进而来。

今天我们不聊某个具体 GC,而是带你系统掌握 4 大基础回收算法的本质与区别,并解释为什么“分代收集”成为主流。


一、标记-清除算法(Mark-Sweep)

核心思想

  1. 首先标记所有存活对象;

  2. 然后清除所有未被标记的对象。

举个例子:

堆内存布局:[ A ][ B ][ C ][ D ]标记阶段 -> A、C 存活清除阶段 -> 释放 B、D

优点

  • 实现简单,基础扎实

  • 不需要复杂的内存移动操作

缺点

  • 容易造成大量碎片

  • 清理效率低,不适合高频回收


二、复制算法(Copying)

核心思想

将内存分为两块,一次只使用一块(From / To 空间),回收时把活的对象复制到另一块,剩下的一整块直接清空。

举个例子:​​​​​​​

From区:[ A ][ B ][ C ]To区:  [ 空 ][ 空 ][ 空 ]标记后 A、C 存活 → 复制到 To 区→ To区:[ A ][ C ][ 空 ]→ From区 全部清空

优点

  • 内存紧凑,天然无碎片

  • 分配速度快(使用指针 bump allocation)

缺点

  • 空间浪费明显(需要备用一块内存)

  • 适用于存活对象比例低的场景


三、标记-整理算法(Mark-Compact)

核心思想

  1. 标记所有存活对象

  2. 将存活对象移动到一侧,并释放后续空间

类似“搬家”:​​​​​​​

原始堆:[ A ][ B ][ C ][ D ][E]标记 A、C、E 存活 → 整理后:[ A ][ C ][ E ][ 空 ][ 空 ]

优点

  • 避免碎片,适合高存活率场景(如老年代)

  • 比复制算法更节省空间

缺点

  • 对象移动开销较大

  • 扫描过程慢于 Copy 算法


四、分代收集算法(Generational Collection)

核心思想

不同“年龄”的对象,用不同的 GC 策略处理。

JVM 内存被划分为:

  • 新生代:频繁创建销毁,使用复制算法(高效率)

  • 老年代:存活时间长,使用标记-整理标记-清除

分代假设背后的两大“经验之谈”:

  • 大多数对象生命周期短(适合复制)

  • 少数对象存活时间长但不动(适合整理)

优点

  • 回收频率与代龄匹配 → 综合性能最佳

  • 实现现代 GC 的基础(G1、ZGC、Shenandoah 等)

缺点

边界跨代引用处理复杂(如 card table / remembered set)


五、算法对比总结表

算法

是否移动对象

有无碎片

适用场景

GC 开销

标记-清除

老年代(少用)

中等

复制算法

新生代

标记-整理

老年代

较高

分代收集

部分

通用架构

最优


六、为什么“分代回收”才是主流选择?

因为它平衡了空间效率 + 回收效率 + 应用响应性三者之间的矛盾。

相比纯标记清除或复制算法,分代收集更具策略性

  • 年轻对象频繁清理,老年对象谨慎处理

  • 结合对象年龄、分配频率、跨代引用行为进行分层优化

现代 GC(如 G1、ZGC)也是在分代模型的基础上进一步优化并发、暂停时间、吞吐量。


七、GC 日志结构全解:如何判断回收是否“健康”?

Java 的 GC 日志,常常被忽视,但它是判断 GC 是否正常运行的第一入口

示例日志(G1 GC):​​​​​​​

[GC pause (G1 Evacuation Pause) (young), 0.0350000 secs]   [Parallel Time: 30.0 ms, GC Workers: 4]      [GC Worker Start (ms): Min: 100, Avg: 105, Max: 110]      [Evacuated: 128M]      [Remaining: 512M]   [Heap after GC: 1G -> 900M]

关键指标:

字段

含义说明

GC 类型

Young GC / Mixed GC / Full GC 等

耗时

是否超过可接受的延迟范围

并发线程数

GC 并发程度,是否存在 GC Worker 偏差

回收前后堆大小

是否真实释放空间,避免“假回收”

Evacuated / Remaining

新生代晋升是否频繁,是否引发老年代膨胀

💡 建议配合 -Xlog:gc*:file=gc.log 启用完整 GC 追踪,并借助GCViewer、gceasy、JClarity等工具可视化分析。


八、典型 GC 问题诊断套路

场景一:频繁 Full GC

可能原因:

  • 新生代过小,频繁晋升

  • 大对象直接进入老年代(需检查 PretenureSizeThreshold)

  • 老年代碎片过多,晋升失败

场景二:GC 停顿时间长

  • 检查是否开启了并发 GC(如 ParallelGC vs CMS vs G1)

  • 检查 GC 日志中 Stop-the-World 时间

  • 若使用 G1,可尝试优化 -XX:MaxGCPauseMillis 与 InitiatingHeapOccupancyPercent

场景三:内存不回收

循环引用误区?→ Java 的 GC 能处理循环引用

实际原因:

  • 引用未断(ThreadLocal、缓存、Map)→ 可配合 MAT 工具分析 GC Roots

  • GC 未触发(内存仍有冗余)→ 人为触发或调整阈值


九、不同 GC 策略选择建议

业务场景

推荐 GC 策略

原因简述

高吞吐计算任务

ParallelGC

吞吐高,延迟容忍

Web 请求服务

G1 GC

延迟低,吞吐均衡

大内存微服务

ZGC / Shenandoah

低延迟 + 高并发适配,最大内存可到 TB 级

老系统兼容

CMS(逐步淘汰)

旧版本 JVM 可能仅支持 CMS

💡 G1 是 Java 11+ 的默认 GC,ZGC 从 Java 15 起也成为可用选项,在 Java 21 LTS 中表现稳定。


十、未来 GC 的发展趋势:ZGC、Shenandoah 与 Region 化内存

现代 GC 不再死守“分代”这套逻辑,它们更关注:

  • 低延迟(<10ms):ZGC 实现了真正并发的 root scan 与标记清理

  • 大堆支持(>4TB):ZGC 支持 64TB 虚拟堆,适配云计算大模型场景

  • Region 式分区:更细粒度划分,提升局部性和并发度(Shenandoah 典型代表)


十一、小结与学习路径建议

学习顺序推荐

  1. 理解四大 GC 基础算法

  2. 学会读懂 GC 日志

  3. 熟悉不同回收器(Serial/Parallel/CMS/G1/ZGC)对比

  4. 掌握 GC 参数配置与调优方法

  5. 能结合 MAT、JFR、JMC、perf 分析内存问题

推荐实践工具

  • VisualVM / JMC:实时分析 GC 行为

  • MAT:分析泄漏对象

  • GCeasy.io / JClarity:GC 日志可视化分析

  • jstat -gcutil、jmap -histo:线上快速排查工具

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小健学 Java

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值