垃圾回收基础¶
垃圾回收是编程语言运行时自动管理内存的机制,核心思想:找出程序不再使用的内存并释放。
什么是垃圾回收¶
垃圾回收(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. 不可预测性¶
你无法精确控制何时释放:
所以 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 用这种。