<!-- title: 谁在替你回收内存：垃圾回收机制 -->
<!-- domain: programming -->
<!-- video: https://youtu.be/c32zXYAK7CI -->

# 谁在替你回收内存：垃圾回收机制

## 为什么这个视频值得你停下来看

写 Java、JavaScript、Go、Python 的程序员，几乎从不主动"释放"内存，可程序跑上几天也不会把内存吃光。这件事细想其实很魔幻——你 `new` 出来的对象，是谁、在什么时候、凭什么判断它"已经没用了"？

答案藏在一个你天天用却几乎从不直视的机制里：**Garbage Collection（垃圾回收，简称 GC）**。Computerphile 频道这期由伦敦国王学院的 **Laurence Tratt 教授**主讲的 *Garbage Collection (Mark & Sweep)*，把这件事最基础也最经典的一种算法——**Mark & Sweep（标记-清除）**——讲得干净利落。它不追新、不炫技，而是回到问题的根源：手动管理内存到底哪里会出错，以及"自动回收"究竟在替你做一道什么样的判断题。

这期视频对我最大的价值是：它把"GC"从一个黑盒名词，还原成了一道**图论问题**。理解了这一层，你写代码时对"为什么这个对象没被回收""为什么程序会卡一下""为什么 Go 的延迟比 Java 低"都会有全新的判断力。

## 视频背景卡片

| 项目 | 内容 |
|------|------|
| 视频标题 | Garbage Collection (Mark & Sweep) |
| 频道 | Computerphile（约 262 万订阅） |
| 时长 | 约 16 分钟 |
| 播放量 | 约 29 万次（截至 2026 年） |
| 发布日期 | 2023 年 1 月 20 日 |
| 主讲人 | Laurence Tratt 教授，伦敦国王学院（King's College London） |
| 核心主题 | 自动内存管理与 Mark & Sweep 垃圾回收算法 |

> Computerphile 是诺丁汉大学旗下的计算机科学科普频道，长期以"大学教授讲透一个概念"的风格著称，是英文世界里最值得收藏的 CS 入门频道之一。Tratt 教授本人是编程语言与内存管理领域的学者，他在视频里推荐的延伸读物是《The Garbage Collection Handbook》。

## 核心内容拆解

### 1. 手动内存管理的"三重诅咒"

视频从一个反差切入：在 C 语言里，内存是你自己 `malloc` 来的，也得自己 `free`。听起来很"自由"，但 Tratt 指出，正是这种自由带来了三类经典且致命的错误：

- **忘了 free（内存泄漏，memory leak）**：对象用完了却没释放，内存一点点被吃光，程序越跑越慢直至 OOM 崩溃。
- **free 了两次（double free）**：同一块内存被释放两次，会破坏堆的管理结构，甚至被攻击者利用（很多早期漏洞都源于此）。
- **free 后还在用（use-after-free / 悬挂指针，dangling pointer）**：指针指向的内存已经被回收并可能被重新分配，程序读到的是"别人家"的数据，行为完全不可预测。

这三类错误之所以难搞，是因为它们往往是**概率性的**——测试时可能好好的，上线后在特定负载下才爆炸。视频的核心动机就是：能不能让运行时自动接管这件事，把这些错误从根本上消灭？

### 2. GC 的核心思想：判断"垃圾"靠的是"不可达"

GC 并不是"定时把所有内存清一遍"那么粗暴。它要回答的真正问题是：**一块内存，到底还"有没有人用"？**

视频传递的关键概念是 **reachability（可达性）**。运行时会维护一组被称为 **GC Roots（根集）** 的引用起点，通常包括：当前各线程的栈帧里的局部变量、全局/静态变量、CPU 寄存器中的引用等。从这些根出发，沿着对象的引用关系一路追下去，**能被追到的对象就是"活的（reachable）"**；追不到的，就是"垃圾（unreachable）"。

这是一个极其优雅的转化：**"还有没有用"这个模糊的问题，被转成了"从根集出发在对象引用图里能不能走到它"这个精确的图遍历问题。**（这是视频的逻辑落点，也是我个人认为整期视频最值得记住的一句。）

### 3. Mark & Sweep：两阶段算法

Tratt 介绍的最经典算法叫 **Mark & Sweep（标记-清除）**，由 John McCarthy 在 1959–1960 年为 Lisp 发明。它分两步：

- **Mark（标记）阶段**：从 GC Roots 出发，深度优先/广度优先地遍历对象引用图，把所有**可达对象**打上一个"存活"标记。
- **Sweep（清除）阶段**：扫描整个堆，凡是**没有被标记**的对象，就是垃圾，把它们占用的内存回收（标记位也复位）。

它最大的优点是**概念简单、不移动对象**——对象的地址在回收前后不变，指针依然有效。但也正因为"不移动"，它留下了一个大麻烦，见下一点。

### 4. Mark & Sweep 的三笔隐藏账

视频点出了 Mark & Sweep 的代价，我把它总结成"三笔账"：

- **Stop-The-World（STW）停顿**：标记和清除期间，对象的引用关系不能被业务线程随意改动，所以运行时往往要"暂停所有应用线程"。这就是你偶尔在 Java 里感受到的程序"卡顿一下"的元凶。
- **内存碎片（fragmentation）**：因为对象不被移动，回收后堆里会留下大量大小不一的"空洞"。下一个稍大的对象可能明明总量够却分配不下去，要么触发又一次 GC，要么需要额外做 compaction（压缩）。
- **堆的额外开销**：Mark & Sweep 通常需要更大的堆才能跑得顺——经验上一个经验法则是"堆至少要是实际存活数据的 2 倍"，否则 GC 会频繁触发。

理解这三笔账，是后面理解所有"现代 GC 进化"的钥匙：后面那些花哨的技术，本质上都是在还这三笔账。

### 5. 演进①：分代假说与分代 GC

（这一段属于我的延伸，视频点到即止。）工程上人们发现了一个经验规律——**分代假说（Generational Hypothesis）**：绝大多数对象都是"朝生夕死"的（弱分代假说）。基于此诞生了**分代垃圾回收（Generational GC）**：把堆分成**年轻代（Young Generation）**和**老年代（Old Generation）**。新对象先进年轻代，年轻代 GC（Minor GC）频繁但很快；熬过几轮回收的"老资格"对象晋升到老年代，老年代 GC（Major/Full GC）不频繁但代价大。

Java 的绝大多数收集器（Serial、Parallel、G1）都建立在分代思想上。这个设计的回报是巨大的：你不必每次都扫描整个堆，只盯住最容易产生垃圾的年轻代即可。

### 6. 演进②：复制收集、压缩与并发回收

（继续延伸。）为了解决碎片和停顿，又演化出几条路线：

- **Copying Collector（复制式收集器）**：把存活对象从一个半区（from-space）**复制**到另一个半区（to-space），复制过程天然就完成了压缩，没有碎片。代价是要浪费一半堆空间，且只适合"存活对象少"的场景——正好配合年轻代使用。
- **Concurrent / Incremental GC（并发/增量回收）**：让 GC 工作和业务线程**同时进行**或**切片进行**，把一次长停顿拆成多次短停顿。Java 的 **G1**（基于 region、可设停顿目标）、**ZGC** 和 **Shenandoah**（追求亚毫秒级停顿、支持超大堆）走的就是这条路；V8 的 **Orinoco** 也用并发标记+并发清除来压低 JS 的卡顿；Go 的 GC 则是并发标记清除，把停顿压到亚毫秒级。

一句话：现代 GC 的军备竞赛，就是围绕"如何在不断堆内存的前提下，把 STW 停顿压到接近零"展开的。

### 7. 另一条路：引用计数（Reference Counting）

视频在"自动 vs 手动"的对比框架里讨论，我在此基础上补一条同样重要的路线——**引用计数（Reference Counting）**。它的思路完全不同：每个对象维护一个计数器，有人引用它就 +1，引用消失就 -1，**一旦归零立即回收**。Python、Objective-C/Swift 的 ARC（Automatic Reference Counting）都是这条路。

引用计数的诱人之处是**确定性回收**（对象一不可用就立刻释放，没有 STW）和**摊销成本**。但它有两个老问题：一是每次指针赋值都要改计数，开销不小；二是**处理不了循环引用**（A 引用 B、B 引用 A，各自计数都是 1，却谁也回收不了）。所以 Python 在引用计数之外，又叠加了一个**专门对付循环引用的分代 tracing GC** 作为补救。

也就是说，**引用计数和 tracing GC 并不是非此即彼，很多运行时是混着用的**。

### 8. 语言哲学：为什么 Rust 选择"没有 GC"

（延伸分析。）讲 GC 就绕不开那个"反潮流"的现代明星：**Rust**。Rust **没有 GC，也不靠引用计数**，而是用**所有权（Ownership）、借用（Borrowing）和生命周期（Lifetime）**，在**编译期**静态地决定每个对象何时该被释放——作用域结束就自动 drop，类似 C++ 的 RAII。

这是一种激进的取舍：它换来了**无运行时、可预测的延迟、适合系统级/嵌入式**，代价是把内存安全的负担在编译期交还给了程序员（于是有了著名的"借用检查器 borrow checker"学习曲线）。

把主流语言摆在一张表上看，内存管理其实是一个光谱：**纯手动（C）→ 引用计数（Python/Swift ARC）→ 分代/并发 tracing GC（Java/JS/Go/C#）→ 编译期所有权（Rust）**。没有谁绝对更好，只有"延迟、吞吐、开发效率、运行时大小"这几个维度的不同权衡。Tratt 的视频帮你理解了 GC 这一端，Rust 则提醒你：GC 不是唯一答案。

## 知识结构图

```mermaid
flowchart TD
    A[程序分配内存] --> B{谁来回收?}
    B -->|手动| C[C/C++: malloc/free]
    C --> C1[泄漏 / Double free / 悬挂指针]
    B -->|自动| D[运行时接管]
    D --> E{主要策略}
    E -->|引用计数 RC| F[Python / Swift ARC]
    F --> F1[计数归零即回收]
    F --> F2[问题: 循环引用]
    E -->|Tracing 追踪式 GC| G[判断: 从 GC Roots 是否可达]
    G --> H[Mark 标记阶段]
    G --> I[Sweep 清除阶段]
    I --> J1[碎片 → Compaction/Copying]
    I --> J2[停顿 STW → 并发/增量 GC]
    H --> K[分代假说: 多数对象朝生夕死]
    K --> L[分代 GC: 年轻代 vs 老年代]
    E -->|编译期所有权| M[Rust: 无 GC]
    M --> M1[所有权+借用+生命周期 → 编译期释放]
```

## 实战启示：理解 GC 如何改变你写代码的方式

看完这个视频，GC 对一个普通程序员的"实战价值"远不止"哦原来如此"。它至少能改变你三层认知。

**第一层：现代语言里照样会"内存泄漏"，只是换了个形态。** GC 只回收"不可达"的对象。如果某个对象你"逻辑上已经不用了"，但某个长期存活的数据结构（比如一个全局 Map、一个缓存、一个监听器列表）还**持有它的引用**，那它就是"可达"的，GC 永远不会碰它——这就是现代语言里的**逻辑泄漏（logical / retention leak）**。排查这类问题，光盯着 `free` 是没用的，你得去找"是谁还揪着它的引用不放"。这条认知能帮你解释 80% 的线上 OOM。

**第二层：热路径上少造对象，是真的有用。** 短命对象虽然会被年轻代 GC 很快清理，但**量太大也会让 Minor GC 频繁触发**，吃掉 CPU。在高频交易、游戏主循环、实时音视频这类对延迟敏感的场景里，"在热路径上复用对象（对象池）、用值类型/结构体替代堆对象、留意逃逸分析（escape analysis）把对象留在栈上"都是真金白银的优化手段。Go、Java、C# 的性能调优文档里反复强调的就是这条。

**第三层：选语言时，把"GC 特性"当成一个可见的维度。** 选型不该只看语法甜不甜。你要构建毫秒级延迟的服务？去看看 Java 的 **ZGC/Shenandoah** 或 Go 的低延迟并发 GC 能否满足你的 SLA；做嵌入式/内核/没有运行时预算的场景？Rust 的"无 GC + 确定性释放"几乎是刚需；写脚本和原型？Python/JS 的 GC 让你根本不必操心。**GC 不是免费的午餐，它的成本体现在延迟毛刺、内存放大和吞吐开销上——只是这部分税，被运行时替你"预扣"了而已。**

## 延伸学习路径

1. **《The Garbage Collection Handbook: The Art of Automatic Memory Management》（第 2 版）** — 视频主讲人 Tratt 教授在视频里亲自推荐的权威读物，Richard Jones、Antony Hosking、Eliot Moss 合著，是 GC 领域的"圣经"。
2. **Oracle 官方文档：Java G1 与 ZGC** — 想理解现代分代 region 化收集器和亚毫秒级低延迟收集器，没有比官方 tuning guide 更准的一手资料。
3. **"Getting to Go 1.5 Garbage Collector"（Go 官方博客，Richard Hudson）** — Go 团队阐述其并发标记清除 GC 的设计目标（停顿 < 500µs），是理解"低延迟 GC 哲学"的经典文章。
4. **V8 团队博客 "Trash talk: the Orinoco garbage collector"** — 看 JavaScript 引擎如何用并发+增量回收，把 GC 停顿从全量 STW 拆解到几乎无感。
5. **Computerphile 频道相关系列** — 在看完本期后，可以顺藤摸瓜看该频道关于内存、指针、操作系统等基础概念的其他视频，形成完整的底层知识链条。

## 一句话总结

> GC 的本质，是把"这块内存还有没有用"这个模糊的人类判断，转化成"从 GC Roots 出发在对象引用图里能不能到达它"这个精确的图遍历问题——理解了这一点，你就拿到了读懂所有现代运行时内存行为的钥匙。

## 参考来源

1. [Garbage Collection (Mark & Sweep) — Computerphile](https://youtu.be/c32zXYAK7CI) — 本文分析的核心视频，Laurence Tratt 教授主讲。
2. [The Garbage Collection Handbook (2nd ed.) — Jones, Hosking, Moss](https://gchandbook.org/) — 视频推荐读物，GC 领域权威专著。
3. [Getting to Go 1.5 Garbage Collector — The Go Blog](https://go.dev/blog/go15gc) — Go 并发 GC 设计与低延迟目标的一手阐述。
4. [Trash talk: the Orinoco garbage collector — V8 Blog](https://v8.dev/blog/orinoco) — V8 并发/增量回收机制详解。
5. [Memory management — MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management) — JavaScript 内存生命周期与可达性概念补充。

---
> 本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵**爱卫生**（微信ID: gprshome201101）维护，聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。
> 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
