.NET的垃圾回收(GC)是CLR提供的自动内存管理机制,负责回收托管堆中不再被引用的对象所占用的内存,避免开发者手动管理内存带来的各种问题。它会自动判断哪些对象还在被使用,哪些已经可以被回收,整个过程对开发者基本透明。

托管堆的基本结构
.NET的托管堆是GC管理的内存区域,所有引用类型的对象都会在这里分配内存。托管堆分为多个段,其中最重要的是小对象堆和大对象堆:
- 小对象堆:存放大小小于85000字节的对象,会按照代龄划分成不同的区域
- 大对象堆:存放大小大于等于85000字节的对象,不会进行代龄提升,回收时直接标记清除
对象在托管堆上的分配是连续的,新对象会直接分配到托管堆的末尾,分配速度非常快,只有当托管堆空间不足时才会触发垃圾回收。
代龄的划分规则
GC采用分代回收的策略来提升回收效率,代龄越高说明对象存活时间越长,被回收的概率越低。托管堆的代龄分为三代:
| 代龄 | 说明 |
|---|---|
| 第0代 | 新创建的对象,还没有经历过垃圾回收,是最容易被回收的一代 |
| 第1代 | 经历过一次垃圾回收后仍然存活的对象,属于短中期存活的对象 |
| 第2代 | 经历过两次及以上垃圾回收后仍然存活的对象,包含大对象堆的所有对象 |
根引用的判定
GC判断对象是否存活的核心是根引用,根引用是指当前还在被程序使用的对象引用,主要包括以下几类:
- 局部变量和方法的参数引用
- 静态变量和静态字段的引用
- CPU寄存器中的引用
- 终结器队列中等待执行的对象引用
如果对象可以通过根引用直接或间接访问到,就说明该对象还在被使用,不会被回收。
垃圾回收的完整流程
一次完整的垃圾回收会经历以下几个步骤:
1. 标记阶段
GC会从所有根引用出发,遍历所有可以访问到的对象,给这些对象做存活标记。没有被标记的对象就是垃圾对象,所占用的内存可以被回收。
2. 清除阶段
GC会回收所有没有被标记的对象的内存空间,将这些空间标记为可用。对于小对象堆,清除后会产生内存碎片,后续会进行压缩。
3. 压缩阶段
GC会把存活的对象移动到托管堆的连续区域,消除内存碎片,同时更新所有指向这些对象的引用地址,保证引用指向正确的位置。
4. 代龄提升
存活下来的对象会提升代龄,第0代存活对象变成第1代,第1代存活对象变成第2代,第2代对象代龄不变。
垃圾回收的触发条件
GC会在以下几种情况下被触发:
- 第0代对象占用的内存空间达到阈值
- 程序手动调用
GC.Collect()方法(不建议常规场景使用) - 系统内存不足时,CLR会触发GC回收内存
- 大对象堆分配新对象时空间不足
简单代码示例
下面的代码展示了对象创建和GC回收的基本过程:
using System;
class Program
{
static void Main()
{
// 创建新对象,分配在第0代
var obj = new TestClass();
Console.WriteLine("对象创建完成");
// 断开引用,obj不再指向TestClass实例
obj = null;
// 手动触发GC(仅演示用,实际开发不建议)
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("GC执行完成");
}
}
class TestClass
{
// 析构函数,对象被回收时会执行
~TestClass()
{
Console.WriteLine("TestClass实例被回收");
}
}
GC优化建议
在实际开发中,可以通过以下方式减少GC的压力:
- 避免不必要的对象创建,尤其是大对象和频繁创建的小对象
- 及时断开不再使用的对象引用,让GC可以更快识别垃圾对象
- 谨慎使用静态变量,静态变量的引用会一直作为根存在,容易导致对象无法被回收
- 避免在析构函数中做耗时操作,会影响GC的执行效率