跳转至

垃圾回收基础

垃圾回收是编程语言运行时自动管理内存的机制,核心思想:找出程序不再使用的内存并释放。


什么是垃圾回收

垃圾回收(Garbage Collection,简称 GC)是编程语言运行时(Runtime)自动管理内存的一种机制。基本判断逻辑:

  • 引用计数 — 每个对象记录有多少东西引用它,归零就回收。Python 用这种。简单高效,但解决不了循环引用。
  • 可达性分析 — 从根对象出发,遍历能走到的所有对象,走不到的就是垃圾。Java、Go、JavaScript 都用这个。

没有 GC 的语言

没有垃圾回收的语言,内存管理靠程序员手动完成:

手动管理

语言 方式
C malloc / free
C++ new / delete,以及 RAII(作用域结束自动析构)
Assembly 直接操作内存,没有抽象层
Fortran 传统上手动管理

所有权 / 借用检查(零成本自动回收,但不是 GC)

语言 机制
Rust 所有权 + 值离开作用域自动释放,编译期检查,无运行时开销
Zig 手动 alloc/free,但提供了 allocator 抽象

其他方式

  • D — 可选 GC,也支持手动管理
  • Swift — ARC(引用计数),严格来说不算传统 GC
  • Objective-C / Swift — ARC 机制

对比理解

GC 语言(Java、Go、Python)
  → 运行时有个"管家"定期打扫
  → 方便,但有 STW 延迟和内存开销

手动管理(C、C++)
  → 自己打扫,扫不干净(泄漏)或扫过头(野指针)都可能出问题

所有权系统(Rust)
  → 编译期就帮你规划好谁负责清理
  → 零运行时开销,但学习曲线陡

GC 的代价

垃圾回收并非免费午餐,主要代价体现在:

1. Stop-The-World(STW)暂停

GC 在某些阶段必须暂停程序执行,所有业务线程停住,等 GC 干完活。

  • 短暂停(几毫秒)对普通应用无所谓
  • 但在高频交易、游戏渲染、实时系统里,几毫秒的抖动就是灾难
  • Java 的 ZGC、Go 的并发 GC 都在拼命缩短这个暂停时间

2. 额外内存开销

GC 需要额外空间来做标记和跟踪:

  • 引用计数:每个对象多一个计数器
  • 三色标记:需要额外的标记位图或栈
  • 分代收集:需要维护多个内存区域和存活/死亡对象的拷贝
  • 写屏障(Write Barrier):每次对象引用变更都要额外记录

实际经验:开销大约在 5%~20% 的额外内存。

3. CPU 时间消耗

GC 线程本身要吃 CPU:

  • 遍历对象图做可达性分析
  • 拷贝存活对象
  • 整理碎片
  • 这些时间本来可以用来跑业务逻辑

Go 的并发 GC 把大部分工作放到后台线程做,但还是要占 CPU。

4. 吞吐量 vs 延迟的矛盾

  • 追求高吞吐:少触发 GC,攒多了一次性清 → 单次暂停更长
  • 追求低延迟:频繁触发 GC → 暂停短但总 CPU 开销更高

这是 GC 调优里永恒的权衡:

收集器 侧重 特点
Parallel GC 高吞吐 暂停长,适合后台批处理
ZGC 超低延迟 暂停 <1ms,吞吐略低
G1 折中 可预测暂停

5. 内存碎片

不同 GC 策略表现不同:

策略 碎片情况
标记-清除 有碎片,长时间运行后分配变慢
标记-压缩 无碎片,但压缩过程要移动对象,暂停更长
复制算法 无碎片,但浪费一半内存空间

碎片多了之后,本来有足够总空闲内存,却分配不出连续的大对象。

6. 不可预测性

你无法精确控制何时释放:

# 你以为这里释放了
obj = None
# 但 GC 可能根本不急着回收
# 文件句柄、数据库连接这种稀缺资源就悬着

所以 GC 语言里经常需要 try-with-resources、with 语句、析构器配合等机制来手动管理资源释放时机。

7. 维护复杂度

GC 的实现本身很复杂——分代阈值怎么设?并发标记和业务线程怎么协调?大对象走哪个分配路径?调优参数一堆,出了问题排查困难。


一句话总结

GC 用 CPU、内存、延迟三样东西,换程序员的脑子。 值不值取决于场景——绝大多数业务应用完全值得;极端性能敏感的场景(嵌入式、高频交易、游戏引擎核心)就可能不值得。

练习题

题目一:哪些语言有 GC?哪些没有?
答案

有 GC:Java、Go、Python、JavaScript、C#、Ruby、Kotlin、Swift(ARC)

无 GC(手动管理):C、C++

无 GC(所有权系统):Rust

可选 GC:D、Swift

题目二:引用计数和可达性分析有什么区别?各有什么优缺点?
答案

引用计数:每个对象维护引用计数器,归零即回收。优点是实现简单、回收及时;缺点是无法处理循环引用(A→B→A),且计数器维护有性能开销。Python 用这种。

可达性分析:从 GC Root 出发遍历对象图,不可达即为垃圾。优点是能处理循环引用;缺点是遍历耗时,可能需要 STW。Java、Go、JavaScript 用这种。

相关链接