在 .NET 中,垃圾回收(Garbage Collection,简称 GC) 是自动管理内存的机制,它会自动识别并释放程序中不再使用的对象所占用的内存,避免内存泄漏,让开发者无需手动分配和释放内存(不像 C/C++ 需要手动调用 free
或 delete
)。
为什么需要垃圾回收?
在没有 GC 的语言中,开发者必须手动管理内存:
- 忘记释放内存 → 内存泄漏(内存越用越多,最终程序崩溃)。
- 释放已释放的内存 → 程序崩溃。
.NET 的 GC 自动解决了这些问题,让开发者更专注于业务逻辑,而非内存管理。
垃圾回收的核心原理
GC 的工作流程可以概括为 “三步曲”:
1. 标记(Mark):识别 “垃圾” 对象
GC 首先会找出所有仍在使用的对象(“存活对象”),剩下的就是 “垃圾对象”(可回收)。
- 如何判断对象是否存活?
通过 “根引用(Roots)” 遍历:- 根引用包括:静态变量、当前方法栈中的局部变量、CPU 寄存器中的对象引用等。
- 从根引用出发,递归标记所有可达的对象(被根引用直接或间接引用的对象)。
- 未被标记的对象就是 “垃圾”(没有任何引用指向它们,程序再也无法访问)。
2. 清理(Sweep):释放垃圾对象的内存
标记完成后,GC 会释放所有未被标记的垃圾对象所占用的内存,将这些内存归还给 “自由内存池”。
3. 压缩(Compact):整理内存碎片
频繁分配和释放内存会导致 “内存碎片”(空闲内存分散成小块,无法容纳大对象)。
GC 会将存活对象移动到一起,紧凑排列,释放出连续的大块内存,避免碎片问题。
垃圾回收的触发时机
GC 由 .NET 运行时自动触发,主要场景包括:
- 内存不足时:当程序申请新内存(如
new
对象)而可用内存不足时,GC 会自动启动。 - 主动调用:开发者可通过
GC.Collect()
手动触发(一般不推荐,除非确有必要)。 - 程序退出时:进程结束时,GC 会回收所有内存。
代(Generations):优化垃圾回收效率
.NET GC 采用 “分代回收” 策略,基于一个观察:大部分对象的生命周期很短(如方法内的局部变量,方法执行完就没用了)。
将对象分为 3 代(Generation 0
、1
、2
),不同代的回收频率不同:
-
第 0 代(Gen 0):新创建的对象(除了大对象)默认属于第 0 代。是 “最新鲜” 的对象。
- 特点:
- 包含所有刚创建且未经历过任何 GC 回收的对象。
- 回收频率最高(每次 GC 几乎都会优先检查 Gen 0),回收速度最快(因为对象数量少且生命周期短)。
- 典型场景:方法内的临时变量(如
string temp = "hello"
)、循环中创建的短期对象等。回收频率最高(每次 GC 几乎都会检查),回收速度最快。
- 特点:
-
第 1 代(Gen 1):第 0 代回收后存活的对象会晋升到第 1 代。
- 特点:
- 作为 “缓冲区”,数量较少,回收频率低于 Gen 0。
- 主要作用是防止短期对象频繁进入长期对象的代(Gen 2),减少 Gen 2 的回收压力。
- 典型场景:在多个方法间传递的临时对象,或在短时间内被多次使用的对象。
- 特点:
-
第 2 代(Gen 2):第 1 代回收后存活的对象会晋升到第 2 代。
- 特点:
- 包含存活时间最长的对象,回收频率最低(可能程序运行期间只回收几次)。
- 第 2 代回收时会同时回收 Gen 0、Gen 1 和 Gen 2 的对象(称为 “全量回收”),开销较大。
- 典型场景:全局缓存对象、静态变量引用的对象、长期存在的业务实体等。
- 特点:
-
大对象堆(LOH):超过一定大小的对象(如大数组,默认 85000 字节)直接分配在大对象堆,属于第 2 代,回收频率更低,且默认不压缩(避免移动大对象的性能开销)。
- 特点:
- 回收频率极低(仅在 Gen 2 回收时才会检查 LOH)。
- 默认不压缩(因为移动大对象会消耗大量性能),可能导致内存碎片(.NET Core 3.0+ 支持手动触发 LOH 压缩)。
- 特点:
分代回收的工作流程
-
优先回收 Gen 0:
当程序申请新内存(如new
对象)时,GC 首先检查 Gen 0。若 Gen 0 内存不足,触发 Gen 0 回收:- 标记 Gen 0 中存活的对象(被根引用或其他存活对象引用)。
- 清理未存活的对象,释放内存。
- 将存活的对象晋升到 Gen 1。
-
Gen 1 回收触发:
当 Gen 1 中的对象数量累积到一定阈值,或 Gen 0 回收后内存仍不足时,触发 Gen 1 回收:- 同时检查 Gen 0 和 Gen 1 的对象。
- 存活的 Gen 0 对象晋升到 Gen 1,存活的 Gen 1 对象晋升到 Gen 2。
-
Gen 2 回收触发:
当 Gen 2 中的对象数量累积到一定阈值,或内存严重不足时,触发 Gen 2 回收(全量回收):- 检查所有代(Gen 0、Gen 1、Gen 2)和 LOH 的对象。
- 存活的对象留在 Gen 2(不再晋升,因为没有更高的代)。
- 此时会压缩内存(LOH 除外,默认不压缩),减少碎片。
垃圾回收对程序的影响
-
暂停执行(STW):GC 执行时,会暂停所有用户线程(“Stop The World”),回收完成后再恢复。
分代回收的优势在于:大部分时间只回收 Gen 0,速度极快,用户几乎感觉不到停顿。 -
性能开销:GC 本身需要消耗 CPU 资源,但现代 .NET(如 .NET Core/5+)的 GC 经过高度优化,性能开销通常很小。
开发者如何配合 GC 工作?
GC 自动管理内存,但开发者可以通过一些最佳实践减少 GC 压力:
-
及时释放不必要的引用:不再使用的对象,尽量将其引用设为
null
(帮助 GC 更快识别垃圾)。List<string> list = new List<string>(); // 使用 list... list = null; // 释放引用,让 GC 可以回收它
-
避免创建过多短期对象:频繁创建和回收小对象会增加 Gen 0 的回收频率(如在循环中
new
对象)。 -
合理使用值类型(struct):值类型存储在栈上(或嵌入到引用类型中),不经过 GC 回收,适合小数据(但滥用会增加栈内存压力)。
-
实现
IDisposable
接口:对于非托管资源(如文件句柄、数据库连接),GC 无法自动回收,需通过IDisposable
手动释放(配合using
语句)。
总结
- 垃圾回收(GC) 是 .NET 自动管理内存的机制,核心是 “标记 - 清理 - 压缩”。
- 通过 “分代回收” 优化性能,优先回收短期对象,减少对程序的影响。
- 开发者无需手动释放内存,但需注意合理使用对象,避免不必要的 GC 压力。
简单说,GC 就像一个 “自动清洁工”,定期清理程序中不再需要的内存 “垃圾”,让程序更稳定、更安全。