专栏文章
专栏文章
GC 垃圾回收系列
1. GC 系列 #01:GC 总览与历史:自动内存管理的起源与演化 2. GC 系列 #02:GC 基础算法:标记-清除、标记-整理、复制收集、引用计数 3. GC 系列 #03:分代假说与分代收集:朝生夕死的工程哲学 4. GC 系列 #04:CMS 收集器:Java 第一个并发收集器的设计与缺陷 5. GC 系列 #05:G1 收集器:Region 化堆与可预测停顿 6. GC 系列 #06:ZGC 与 Shenandoah:亚毫秒级停顿的并发移动收集器 7. GC 系列 #07:Go 的 GC:并发三色标记与混合写屏障 8. GC 系列 #08:Python 的内存管理:引用计数与分代循环 GC 9. GC 系列 #09:GC 调优实践:JVM 日志解读与 G1/ZGC 参数策略

GC 系列 #02:GC 基础算法:标记-清除、标记-整理、复制收集、引用计数

发布于 2026-05-25 14:15 👁 28 次阅读
#GC#内存管理#理论#算法

所有现代 GC 都建立在四种基础算法之上:标记-清除、标记-整理、复制收集、引用计数。理解它们的原理、优缺点和适用场景,是理解 G1、ZGC 等复杂收集器的前提。


目录

章节 说明
可达性分析 如何判断对象是否存活
标记-清除 最古老的 GC 算法
标记-整理 解决碎片问题
复制收集 高效但费内存
引用计数 即时回收,但有循环引用问题
算法对比 各维度横向比较

可达性分析

所有追踪式 GC(标记-清除/整理/复制)的第一步都是找出哪些对象是存活的

GC Roots(根集合)

从以下"根"出发,可达的对象都是存活的,不可达的是垃圾:

GC Roots 包括:栈帧局部变量、静态变量、JNI 引用、活跃线程对象。

gc reachability

可达性 vs 引用计数

可达性分析(Reachability Analysis)和引用计数(Reference Counting)是判断"存活"的两类思路:

可达性分析 引用计数
判断时机 GC 触发时批量判断 引用变化时立即判断
循环引用 ✅ 能处理 ❌ 无法处理(需额外机制)
暂停 可能有 STW 通常无
开销 集中在 GC 时 分散在每次引用操作

标记-清除

Mark-Sweep,1959 年 McCarthy 为 Lisp 发明,是所有 GC 算法的始祖。

两个阶段

标记阶段(Mark): 从 GC Roots 出发,遍历所有可达对象,打上标记;清除阶段(Sweep): 扫描整个堆,回收未标记的对象。

gc mark sweep

核心问题:内存碎片

碎片导致大对象分配失败,并触发更频繁的 GC。例如:回收 B、C 后总空闲 35B,但无法分配连续 25B 的对象。


标记-整理

Mark-Compact,在标记-清除基础上增加整理(Compact) 阶段,解决碎片问题。

三个阶段

gc mark compact

代价

整理需要移动对象,移动后所有指向这些对象的引用都需要更新——这是一个昂贵的操作,通常需要 STW(Stop-The-World,暂停所有应用线程)。

gc compact ref update


复制收集

Copying Collection,将堆分为两个等大的半区,每次只使用一半。

工作原理

gc copying

优缺点

优点:

缺点:

这就是为什么复制收集主要用于年轻代(朝生夕死,存活率低,复制开销小)。

gc bump pointer


引用计数

每个对象维护一个计数器,记录有多少引用指向它。计数降为 0 时立即回收。

工作原理

# Python 引用计数示意
a = [1, 2, 3]   # 列表对象引用计数 = 1
b = a            # 引用计数 = 2
del a            # 引用计数 = 1
del b            # 引用计数 = 0 → 立即回收

循环引用问题

# 循环引用导致内存泄漏
a = {}
b = {}
a['b'] = b   # b 的引用计数 = 2(变量 b + a 字典内的引用)
b['a'] = a   # a 的引用计数 = 2(变量 a + b 字典内的引用)

del a        # a 的引用计数 = 1(b 字典仍然引用它,从 2 降到 1)
del b        # b 的引用计数 = 1(a 字典仍然引用它,从 2 降到 1)
# 计数无法降为 0,触发不了回收
# 但它们从任何 GC Root 都不可达
# → 内存泄漏!

Python 用分代循环检测(cyclic garbage collector)解决这个问题,周期性地检测不可达的循环引用。

Swift ARC(自动引用计数)

class Node {
    var next: Node?   // strong 引用:计数 +1

    // 循环引用的解决:weak 引用不增加计数
    weak var parent: Node?
}

var a: Node? = Node()   // 引用计数 = 1
var b: Node? = a        // 引用计数 = 2
a = nil                 // 引用计数 = 1
b = nil                 // 引用计数 = 0 → 释放

算法对比

算法 吞吐量 延迟 内存占用 碎片 适用场景
标记-清除 高(STW) 严重 已基本淘汰(作为基础)
标记-整理 高(STW+移动) 老年代(Serial Old)
复制收集 低(存活少时) 高(50%浪费) 年轻代(Eden/Survivor)
引用计数 高(分散) 极低(即时) Python、Swift、Rust Rc

现代 GC 都是组合算法: G1 在年轻代用复制收集,在老年代用标记-整理;ZGC 在标记-整理的基础上引入染色指针和读屏障实现并发移动。

参考资料

← 返回列表

评论 (1)

manfred 2026-05-26 02:58
👍

发表评论