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

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

发布于 2026-05-25 14:15 · 最后编辑于 2026-07-31 15:53 · 字数 1,392 👁 109 次阅读

所有现代 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

优缺点

优点:

  • 分配极快(指针碰撞,bump pointer)
  • 回收后空间连续,无碎片
  • 只需遍历存活对象,垃圾多时反而快

缺点:

  • 内存利用率只有 50%
  • 存活对象多时复制开销大

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

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
👍
登录注册 后才能发表评论