垃圾回收(GC)是自动内存管理的核心机制,让程序员从手动 malloc/free 的陷阱中解放出来。本文梳理 GC 的起源动机、核心权衡,以及各主流语言的选择背景。
目录
| 章节 | 说明 |
|---|---|
| 为什么需要 GC | 手动管理的问题 |
| GC 的核心权衡 | 吞吐量、延迟、内存占用 |
| 历史沿革 | 从 Lisp 到现代 GC |
| 各语言的选择 | 为什么不是所有语言都用 GC |
为什么需要 GC
手动内存管理的两类致命错误
悬空指针(Use-After-Free): 释放内存后继续使用
int *p = malloc(sizeof(int));
*p = 42;
free(p); // 释放内存
printf("%d\n", *p); // ❌ 未定义行为:内存已被释放
// 可能读到垃圾值,可能崩溃,可能被利用为安全漏洞
内存泄漏(Memory Leak): 应该释放的内存没有释放
void process() {
char *buf = malloc(1024);
if (error_condition) {
return; // ❌ 忘记 free(buf),每次调用泄漏 1KB
}
// ...
free(buf);
}
// 长期运行的服务器程序最终 OOM
这两类错误在 C/C++ 中是最常见、最难调试的 Bug。GC 的根本价值是消除这两类错误。
GC 的代价
GC 不是免费的——它用CPU 时间和额外内存换取程序员的认知负担:
| 手动管理 | GC | |
|---|---|---|
| 内存控制精度 | 精确 | 不精确(GC 决定何时回收) |
| CPU 开销 | 低(只有 malloc/free) | 有额外扫描开销 |
| 内存峰值 | 低(释放立即生效) | 高(GC 不立即回收) |
| 停顿 | 无 | 可能有 STW(Stop-The-World) |
| 安全性 | 低(悬空指针/泄漏) | 高(自动管理) |
GC 的核心权衡
所有 GC 算法都在三个维度上取舍(来源:《垃圾回收算法手册》,工程实践经验总结):
- 吞吐量(Throughput):应用线程占 CPU 时间的比例,GC 线程占用越少越好
- 停顿延迟(Pause Latency):单次 GC 导致应用暂停的最长时间
- 空间开销(Space Overhead):GC 机制本身需要的额外内存,如引用计数字段、卡表、记忆集、转发表等辅助数据结构
三者无法同时最优,任意 GC 都在其中两项优秀、一项妥协:
| GC 类型 | 优先 | 牺牲 | 代表 |
|---|---|---|---|
| 吞吐量优先 | 吞吐量 | 停顿延迟(长 STW) | Serial GC、Parallel GC |
| 延迟优先 | 停顿延迟 | 吞吐量(并发 GC 线程开销)+ 空间开销(转发表等) | ZGC、Shenandoah |
| 空间开销优先 | 空间开销 | 吞吐量(原子操作/并发开销) | 引用计数(CPython、Swift ARC) |
| 均衡 | 三者折中 | 实现复杂度 | G1 |
历史沿革
| 年份 | 事件 |
|---|---|
| 1959 | John McCarthy 在 Lisp 中发明 GC(标记-清除算法) |
| 1960 | George Collins 提出引用计数 |
| 1963 | Marvin Minsky 提出复制收集算法(Cheney 1970 改进) |
| 1970s | Smalltalk 推广分代 GC 思想 |
| 1984 | David Ungar 发表分代假说论文 |
| 1990s | Java 兴起,GC 进入主流工业应用 |
| 1994 | CMS(Concurrent Mark-Sweep)思想提出 |
| 2000 | Java HotSpot 引入分代收集 |
| 2004 | Azul Systems 发布 Pauseless GC(ZGC 的精神前身) |
| 2006 | G1 GC 在研究论文中发表 |
| 2012 | G1 在 Java 7u4 正式发布 |
| 2018 | ZGC 在 JDK 11 作为实验特性发布 |
| 2020 | ZGC 在 JDK 15 正式发布(亚毫秒停顿) |
| 2021 | Go 1.17 并发 GC 成熟,停顿 < 1ms |
关键转折点
1959 Lisp GC: McCarthy 设计 Lisp 时意识到手动管理与函数式语言的不兼容性——函数式编程大量产生临时对象,手动管理不可行。GC 是函数式范式的必然产物。
1984 分代假说: David Ungar 观察到"大多数对象年轻时死亡",这一洞察使分代 GC 成为主流,性能提升数倍。
2004 Pauseless GC: Azul 系统在特定硬件上实现了完全无停顿 GC,证明了理论可行性,为 ZGC 等指明了方向。
各语言的选择
| 语言 | GC 策略 | 核心原因 |
|---|---|---|
| Java/JVM | 分代 GC(G1/ZGC) | 企业级应用,内存安全优先于极致性能 |
| Go | 并发三色标记 | 系统编程,低延迟是核心需求,不分代 |
| Python | 引用计数 + 分代 | 简单实现,C 扩展可控制引用 |
| JavaScript (V8) | 分代(Orinoco) | 浏览器环境,用户感知延迟敏感 |
| C#/.NET | 分代(Gen0/1/2) | 与 Java 类似定位 |
| Rust | 无 GC(所有权系统) | 系统编程,编译时保证内存安全 |
| C/C++ | 无 GC(手动) | 极致控制,底层系统 |
| Swift | ARC(自动引用计数) | Apple 生态,确定性析构 |
| Haskell | 分代(GHC GC) | 惰性求值产生大量临时对象,GC 必要 |
为什么 Rust 不用 GC
Rust 用所有权(Ownership)+ 借用检查器(Borrow Checker) 在编译期解决内存安全问题:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移交给 s2
// println!("{}", s1); // ❌ 编译错误:s1 已被移走
println!("{}", s2); // ✅
} // s2 离开作用域,自动调用 drop,释放内存
// 编译器保证:无悬空指针,无内存泄漏,无 GC 开销
代价是学习曲线陡峭,所有权规则对开发者的认知要求高。
参考资料
- John McCarthy 1960 年论文:"Recursive Functions of Symbolic Expressions"(GC 的诞生)
- David Ungar & Randall Smith 1987 年论文:"Generation Scavenging"(分代 GC)
- 《深入理解 Java 虚拟机》第 3 章 — 周志明
- GC 基础算法
评论 (0)
发表评论