专栏文章
专栏文章
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 系列 #01:GC 总览与历史:自动内存管理的起源与演化

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

垃圾回收(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 算法都在三个维度上取舍(来源:《垃圾回收算法手册》,工程实践经验总结):

三者无法同时最优,任意 GC 都在其中两项优秀、一项妥协:

gc triangle

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)

暂无评论,来留下第一条吧。

发表评论