写 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》。
视频从一个反差切入:在 C 语言里,内存是你自己 malloc 来的,也得自己 free。听起来很"自由",但 Tratt 指出,正是这种自由带来了三类经典且致命的错误:
这三类错误之所以难搞,是因为它们往往是概率性的——测试时可能好好的,上线后在特定负载下才爆炸。视频的核心动机就是:能不能让运行时自动接管这件事,把这些错误从根本上消灭?
GC 并不是"定时把所有内存清一遍"那么粗暴。它要回答的真正问题是:一块内存,到底还"有没有人用"?
视频传递的关键概念是 reachability(可达性)。运行时会维护一组被称为 GC Roots(根集) 的引用起点,通常包括:当前各线程的栈帧里的局部变量、全局/静态变量、CPU 寄存器中的引用等。从这些根出发,沿着对象的引用关系一路追下去,能被追到的对象就是"活的(reachable)";追不到的,就是"垃圾(unreachable)"。
这是一个极其优雅的转化:"还有没有用"这个模糊的问题,被转成了"从根集出发在对象引用图里能不能走到它"这个精确的图遍历问题。(这是视频的逻辑落点,也是我个人认为整期视频最值得记住的一句。)
Tratt 介绍的最经典算法叫 Mark & Sweep(标记-清除),由 John McCarthy 在 1959–1960 年为 Lisp 发明。它分两步:
它最大的优点是概念简单、不移动对象——对象的地址在回收前后不变,指针依然有效。但也正因为"不移动",它留下了一个大麻烦,见下一点。
视频点出了 Mark & Sweep 的代价,我把它总结成"三笔账":
理解这三笔账,是后面理解所有"现代 GC 进化"的钥匙:后面那些花哨的技术,本质上都是在还这三笔账。
(这一段属于我的延伸,视频点到即止。)工程上人们发现了一个经验规律——分代假说(Generational Hypothesis):绝大多数对象都是"朝生夕死"的(弱分代假说)。基于此诞生了分代垃圾回收(Generational GC):把堆分成年轻代(Young Generation)和老年代(Old Generation)。新对象先进年轻代,年轻代 GC(Minor GC)频繁但很快;熬过几轮回收的"老资格"对象晋升到老年代,老年代 GC(Major/Full GC)不频繁但代价大。
Java 的绝大多数收集器(Serial、Parallel、G1)都建立在分代思想上。这个设计的回报是巨大的:你不必每次都扫描整个堆,只盯住最容易产生垃圾的年轻代即可。
(继续延伸。)为了解决碎片和停顿,又演化出几条路线:
一句话:现代 GC 的军备竞赛,就是围绕"如何在不断堆内存的前提下,把 STW 停顿压到接近零"展开的。
视频在"自动 vs 手动"的对比框架里讨论,我在此基础上补一条同样重要的路线——引用计数(Reference Counting)。它的思路完全不同:每个对象维护一个计数器,有人引用它就 +1,引用消失就 -1,一旦归零立即回收。Python、Objective-C/Swift 的 ARC(Automatic Reference Counting)都是这条路。
引用计数的诱人之处是确定性回收(对象一不可用就立刻释放,没有 STW)和摊销成本。但它有两个老问题:一是每次指针赋值都要改计数,开销不小;二是处理不了循环引用(A 引用 B、B 引用 A,各自计数都是 1,却谁也回收不了)。所以 Python 在引用计数之外,又叠加了一个专门对付循环引用的分代 tracing GC 作为补救。
也就是说,引用计数和 tracing 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 不是唯一答案。
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 只回收"不可达"的对象。如果某个对象你"逻辑上已经不用了",但某个长期存活的数据结构(比如一个全局 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 不是免费的午餐,它的成本体现在延迟毛刺、内存放大和吞吐开销上——只是这部分税,被运行时替你"预扣"了而已。
GC 的本质,是把"这块内存还有没有用"这个模糊的人类判断,转化成"从 GC Roots 出发在对象引用图里能不能到达它"这个精确的图遍历问题——理解了这一点,你就拿到了读懂所有现代运行时内存行为的钥匙。
本文由「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 等场景全覆盖。
