垃圾回收器是 JVM 的核心“管家”,但它并不神秘,而是基于几种经典算法演进而来。
今天我们不聊某个具体 GC,而是带你系统掌握 4 大基础回收算法的本质与区别,并解释为什么“分代收集”成为主流。
一、标记-清除算法(Mark-Sweep)
核心思想:
-
首先标记所有存活对象;
-
然后清除所有未被标记的对象。
举个例子:
堆内存布局:[ 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)
核心思想:
-
标记所有存活对象
-
将存活对象移动到一侧,并释放后续空间
类似“搬家”:
原始堆:[ 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 典型代表)
十一、小结与学习路径建议
学习顺序推荐:
-
理解四大 GC 基础算法
-
学会读懂 GC 日志
-
熟悉不同回收器(Serial/Parallel/CMS/G1/ZGC)对比
-
掌握 GC 参数配置与调优方法
-
能结合 MAT、JFR、JMC、perf 分析内存问题
推荐实践工具:
-
VisualVM / JMC:实时分析 GC 行为
-
MAT:分析泄漏对象
-
GCeasy.io / JClarity:GC 日志可视化分析
-
jstat -gcutil、jmap -histo:线上快速排查工具


1万+

被折叠的 条评论
为什么被折叠?



