一、什么是垃圾回收(Garbage Collection)?
1.1 背景:内存管理的挑战
在程序运行过程中,会动态分配内存来存储对象、数据结构等。传统上,程序员需要手动管理内存(如 C/C++ 中的 malloc/free 或 new/delete),这容易导致:
- 内存泄漏(Memory Leak):忘记释放不再使用的内存,导致内存持续增长。
- 悬空指针(Dangling Pointer):释放了内存但仍有指针指向它,后续访问会导致未定义行为。
- 双重释放(Double Free):重复释放同一块内存,可能导致程序崩溃。
为了解决这些问题,自动垃圾回收机制应运而生。GC 的目标是自动识别并回收程序中不再使用的内存,从而减轻程序员负担,提高程序的健壮性和安全性。
二、GC 的基本原理
GC 的核心任务是:识别哪些内存是“垃圾”(即不再被程序使用的对象),并将其释放回内存池。
2.1 可达性分析(Reachability Analysis)
现代 GC 普遍采用可达性分析来判断对象是否存活。基本思想是:
- 从一组称为 GC Roots 的对象开始,通过引用链向下遍历。
- 所有能从 GC Roots 直接或间接访问到的对象被认为是存活的。
- 无法到达的对象则被认为是垃圾,可以被回收。
常见的 GC Roots 包括:
- 虚拟机栈(栈帧中的局部变量表) 中引用的对象。
- 方法区 中的类静态属性引用的对象。
- 方法区 中常量引用的对象。
- 本地方法栈 中 JNI(Java Native Interface)引用的对象。
- 活跃的线程 对象。
- 被 synchronized 锁持有的对象(在某些 JVM 实现中)。
举例:一个局部变量
obj指向一个对象 A,A 又指向对象 B,B 指向对象 C。如果obj是 GC Roots 之一,那么 A、B、C 都是可达的,不会被回收。如果obj被置为null,那么 A、B、C 都不可达,成为垃圾。
三、主流的垃圾回收算法
GC 的实现依赖于几种经典算法,它们各有优劣,通常在实际系统中组合使用。
3.1 标记-清除(Mark-Sweep)
- 标记(Mark):从 GC Roots 开始遍历所有可达对象,标记为“存活”。
- 清除(Sweep):遍历整个堆,将未被标记的对象回收。
- 优点:实现简单。
- 缺点:
- 产生内存碎片,可能导致大对象无法分配。
- 效率不高,尤其是堆很大时。
3.2 复制(Copying)
- 将内存分为两个相等的区域:From 空间 和 To 空间。
- 新对象分配在 From 空间。
- 当 From 空间满时,进行 GC:
- 将 From 空间中所有存活对象复制到 To 空间。
- 清空 From 空间,然后交换 From 和 To。
- 优点:
- 没有内存碎片。
- 复制过程同时完成整理。
- 缺点:
- 内存利用率只有 50%。
- 适合存活对象少的场景(如新生代)。
典型应用:JVM 中的 新生代(Young Generation) 常使用复制算法。
3.3 标记-整理(Mark-Compact)
- 标记:同标记-清除。
- 整理(Compact):将所有存活对象向内存一端移动,然后清理边界以外的内存。
- 优点:避免内存碎片,内存利用率高。
- 缺点:整理过程耗时,移动对象需要更新引用。
典型应用:JVM 中的 老年代(Old Generation) 常使用标记-整理或其变种。
3.4 分代收集(Generational Collection)
这是现代 GC 最核心的思想,基于弱代假说(Weak Generational Hypothesis):
- 大多数对象都是“朝生夕死”的(短命)。
- 老对象引用新对象的情况较少。
分代设计:
- 新生代(Young Generation):
- 存放新创建的对象。
- 使用复制算法,GC 频繁(Minor GC)。
- 通常分为 Eden 区和两个 Survivor 区(S0, S1)。
- 老年代(Old Generation):
- 存放长期存活的对象。
- 使用标记-清除或标记-整理,GC 较少(Major GC / Full GC)。
- 永久代 / 元空间(Metaspace):
- 存放类元数据、常量池等(Java 8 后永久代被元空间取代)。
对象晋升:
- 对象在 Eden 区创建。
- Minor GC 后,存活对象复制到 Survivor 区。
- 经过多次 GC 仍存活的对象,晋升到老年代。
- 大对象可能直接进入老年代。
四、现代 GC 的实现与优化
4.1 JVM 中的主流 GC 器(以 HotSpot 为例)
| GC 名称 | 算法 | 特点 |
|---|---|---|
| Serial GC | 单线程,新生代复制,老年代标记-整理 | 简单,适合单核、小内存应用 |
| Parallel GC(吞吐量优先) | 多线程并行收集 | 提高吞吐量,适合后台计算 |
| CMS(Concurrent Mark-Sweep) | 并发标记清除 | 减少停顿时间,但有碎片和并发失败风险(已废弃) |
| G1(Garbage-First) | 分区 + 并发 + 增量整理 | 面向大堆,可预测停顿时间,兼顾吞吐和延迟 |
| ZGC(Z Garbage Collector) | 并发、低延迟(<10ms) | 支持超大堆(TB 级),停顿时间极短 |
| Shenandoah | 并发压缩,低延迟 | 与 ZGC 类似,独立开发 |
4.2 关键技术与优化
4.2.1 写屏障(Write Barrier)
- 用于在对象引用更新时,通知 GC 记录变化。
- 支持并发标记(如 CMS、G1、ZGC)。
- 类型:增量更新(Incremental Update)、SATB(Snapshot-At-The-Beginning)。
4.2.2 三色标记法(Tri-color Marking)
- 将对象分为三种颜色:
- 白色:未访问,可能是垃圾。
- 灰色:已访问,但其引用的对象还未处理。
- 黑色:已访问,且其引用的对象也已处理。
- GC 从 GC Roots 开始,将对象从白 → 灰 → 黑。
- 最终所有白色对象为垃圾。
并发标记中的问题:对象引用关系变化可能导致漏标(漏掉本该标记的对象)。
- 增量更新:打破“黑→白”引用时,将白色对象重新标记为灰色。
- SATB:在标记开始时记录快照,后续新增的“黑→白”引用视为已断开。
4.2.3 卡表(Card Table)与记忆集(Remembered Set)
- 用于解决跨代引用问题(老年代对象引用新生代对象)。
- 卡表:将老年代划分为固定大小的“卡”(Card),记录哪些卡包含跨代引用。
- 记忆集:为每个区域维护一个集合,记录从外部指向该区域的引用。
- 在新生代 GC 时,只需扫描记忆集中的卡,而无需扫描整个老年代。
4.2.4 并发与并行
- 并行(Parallel):多个 GC 线程同时工作,但会暂停应用线程(Stop-The-World)。
- 并发(Concurrent):GC 线程与应用线程同时运行,减少停顿时间(如 ZGC、Shenandoah)。
4.2.5 内存整理与压缩
- G1、ZGC、Shenandoah 都支持并发压缩,避免内存碎片。
- ZGC 使用染色指针(Colored Pointers) 技术,将状态信息(如标记位)存储在指针中,减少内存开销。
五、其他语言的 GC 实现
5.1 Go 语言
- 使用三色标记 + 混合写屏障(Hybrid Write Barrier)。
- 支持并发标记和并发清除。
- STW(Stop-The-World)时间极短(通常 < 1ms)。
- 内存管理基于逃逸分析,部分对象在栈上分配,减少 GC 压力。
5.2 Python
- 主要使用引用计数(Reference Counting) + 循环垃圾检测器(Cycle Detector)。
- 引用计数:每个对象维护引用计数,为 0 时立即回收。
- 缺点:无法处理循环引用(如 A 引用 B,B 引用 A)。
- 循环检测器:定期使用标记-清除算法检测并回收循环引用。
5.3 JavaScript(V8 引擎)
- 使用分代收集 + 标记-清除/整理。
- 新生代:Scavenge(复制算法)。
- 老年代:Mark-Sweep + Mark-Compact。
- 支持增量标记、并发标记、并行回收等优化。
- 内存限制(通常 1.4GB 左右),适合浏览器环境。
5.4 .NET(C#)
- 类似 JVM,使用分代 GC(Gen0, Gen1, Gen2)。
- 支持多种 GC 模式:工作站 GC(交互式)、服务器 GC(高吞吐)。
- 使用精确 GC(Precise GC),能准确识别对象引用。
六、GC 的优缺点
优点:
- 自动管理内存,减少内存泄漏和悬空指针。
- 提高开发效率和程序安全性。
- 支持高级语言特性(如闭包、动态类型)。
缺点:
- 性能开销:GC 运行时消耗 CPU 和内存资源。
- 停顿时间(Pause Time):Stop-The-World 可能导致程序暂停,影响实时性。
- 内存占用:GC 需要额外内存(如双倍新生代、卡表、记忆集等)。
- 不可预测性:GC 何时触发、耗时多久难以精确控制。
七、如何优化 GC 性能?
- 合理设置堆大小:避免过小导致频繁 GC,过大导致回收时间长。
- 选择合适的 GC 器:根据应用类型(低延迟、高吞吐)选择 G1、ZGC 等。
- 减少对象创建:复用对象、使用对象池、避免短命大对象。
- 避免过早晋升:控制新生代大小和晋升阈值。
- 监控与调优:使用
jstat、jmap、VisualVM、Prometheus等工具监控 GC 行为。 - 代码层面优化:及时断开引用、避免长生命周期持有短命对象。
八、总结
垃圾回收是现代编程语言的核心基础设施之一。它通过可达性分析识别垃圾,利用分代收集、复制、标记-整理等算法高效回收内存。随着硬件发展,GC 技术也在不断演进,从简单的单线程收集器发展到支持并发、低延迟、大堆的现代 GC(如 ZGC、Shenandoah)。
9615

被折叠的 条评论
为什么被折叠?



