第 4 章:垃圾回收(GC)深入剖析
jerry北京市2026年4月10日C# 13 次阅读 约 17 分钟

深入理解 .NET GC 的分代回收机制、GC Root、终结器队列、IDisposable 模式、大对象堆和 GC 调优。
4.1 分代回收
.NET GC 将堆分为三代:
┌─────────────────────────────────────────────────┐
│ 托管堆 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Gen 0 │ │ Gen 1 │ │ Gen 2 │ │
│ │ 新对象 │ │ 存活过1次 │ │ 长期存活 │ │
│ │ ~256KB │ │ ~2MB │ │ 无上限 │ │
│ │ 回收最频繁│ │ 中等频率 │ │ 回收最少 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ LOH (Large Object Heap) │ │
│ │ ≥ 85,000 bytes 的对象 │ │
│ │ 等同于 Gen 2 回收 │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ POH (Pinned Object Heap) .NET 5+ │ │
│ │ 固定的对象 │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
分代假设
GC 基于"分代假设"(Generational Hypothesis):
- 大多数对象生命周期很短(临时变量、方法内的对象)
- 存活越久的对象,越可能继续存活
回收过程
Gen 0 回收(最频繁):
1. 暂停所有线程(STW - Stop The World)
2. 从 GC Root 开始标记 Gen 0 中可达的对象
3. 存活的对象提升到 Gen 1
4. Gen 0 空间被回收
Gen 1 回收:
- 同时回收 Gen 0 和 Gen 1
- 存活的 Gen 1 对象提升到 Gen 2
Gen 2 回收(Full GC,最慢):
- 回收所有代 + LOH
- 尽量避免触发
4.2 GC Root
GC 从 Root 开始遍历,能到达的对象是"存活"的,不能到达的是"垃圾":
// GC Root 包括:
// 1. 栈上的局部变量和方法参数
void Method()
{
var obj = new MyClass(); // obj 是 GC Root
// obj 在 Method 执行期间不会被回收
}
// 2. 静态字段
static MyClass _instance = new MyClass(); // 永远是 Root
// 3. GC Handle(GCHandle.Alloc)
// 4. 终结器队列中的对象
// 5. CPU 寄存器中的引用
常见的内存泄漏
// 1. 静态集合持有引用
static List<object> _cache = new();
void Process()
{
_cache.Add(new LargeObject()); // 永远不会被回收
}
// 2. 事件未取消订阅
class Publisher
{
public event EventHandler MyEvent;
}
class Subscriber
{
public Subscriber(Publisher pub)
{
pub.MyEvent += OnEvent; // Publisher 持有 Subscriber 的引用
// 如果 Publisher 生命周期比 Subscriber 长,Subscriber 不会被回收
}
// 应该在不需要时取消订阅:pub.MyEvent -= OnEvent;
}
// 3. 闭包捕获
void Method()
{
var largeData = new byte[1024 * 1024];
Action action = () => Console.WriteLine(largeData.Length);
// action 持有 largeData 的引用
// 只要 action 存活,largeData 就不会被回收
}
4.3 终结器(Finalizer)
class ResourceHolder
{
~ResourceHolder() // 终结器(析构函数语法)
{
// 释放非托管资源
CloseHandle(_handle);
}
}
终结器的执行流程:
1. 对象不可达时,不会立即回收
2. 被放入终结器队列(Finalization Queue)
3. 终结器线程执行终结器方法
4. 下一次 GC 时才真正回收
结果:有终结器的对象至少存活两次 GC,提升到更高的代
IDisposable 模式(推荐)
class ManagedResource : IDisposable
{
private IntPtr _handle; // 非托管资源
private Stream _stream; // 托管资源
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // 告诉 GC 不需要调用终结器了
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// 释放托管资源
_stream?.Dispose();
}
// 释放非托管资源
if (_handle != IntPtr.Zero)
{
CloseHandle(_handle);
_handle = IntPtr.Zero;
}
_disposed = true;
}
~ManagedResource()
{
Dispose(false); // 终结器只释放非托管资源
}
}
// 使用 using 语句自动调用 Dispose
using var resource = new ManagedResource();
// 离开作用域时自动调用 Dispose()
大多数情况下,如果只有托管资源,简化版就够了:
class SimpleResource : IDisposable
{
private Stream? _stream;
public void Dispose()
{
_stream?.Dispose();
_stream = null;
}
}
4.4 大对象堆(LOH)
≥ 85,000 字节的对象分配在 LOH 上:
// 这个数组会分配在 LOH 上
var largeArray = new byte[85000]; // ≥ 85000 bytes → LOH
// double[] 的阈值更低
var doubles = new double[1000]; // 1000 * 8 = 8000 bytes → SOH
var doubles2 = new double[10625]; // 10625 * 8 = 85000 bytes → LOH
LOH 的特点:
- 等同于 Gen 2 回收(Full GC 才回收)
- 默认不压缩(可能产生碎片)
- .NET 4.5.1+ 可以手动请求压缩
// 请求 LOH 压缩(下次 Full GC 时执行)
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
避免 LOH 碎片的方法:
- 使用
ArrayPool<T>.Shared复用大数组 - 避免频繁分配和释放大对象
4.5 GC 模式
// 工作站 GC(Workstation GC)
// - 默认用于桌面应用
// - 单线程 GC
// - 更低的延迟
// 服务器 GC(Server GC)
// - 用于服务端应用
// - 每个 CPU 核心一个 GC 线程
// - 更高的吞吐量
<!-- .csproj 配置 -->
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>
// runtimeconfig.json
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true,
"System.GC.Concurrent": true
}
}
}
4.6 GC 调优
// 手动触发 GC(通常不推荐)
GC.Collect(); // 回收所有代
GC.Collect(0); // 只回收 Gen 0
GC.Collect(2, GCCollectionMode.Optimized); // 让 GC 决定是否需要回收
// 查看 GC 信息
GC.GetGCMemoryInfo(); // 详细的 GC 内存信息
GC.GetTotalMemory(false); // 当前托管堆大小
GC.CollectionCount(0); // Gen 0 回收次数
// 注册 GC 通知
GC.RegisterForFullGCNotification(10, 10);
// .NET 6+ 控制 GC 暂停时间
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
减少 GC 压力的方法
// 1. 使用对象池
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024);
try { /* 使用 buffer */ }
finally { pool.Return(buffer); }
// 2. 使用 ValueTask 代替 Task(减少堆分配)
async ValueTask<int> GetValueAsync() { ... }
// 3. 使用 Span<T> 避免数组分配
Span<int> span = stackalloc int[100]; // 栈上分配
// 4. 使用 struct 代替 class(小对象)
readonly record struct Point(int X, int Y);
// 5. 缓存和复用对象
private static readonly StringBuilder _sb = new();
4.7 面试要点
-
.NET GC 的分代机制?
- Gen 0(新对象)、Gen 1(缓冲区)、Gen 2(长期存活)
- 基于分代假设:大多数对象生命周期很短
-
什么是 GC Root?
- 栈上的局部变量、静态字段、GC Handle、终结器队列
-
终结器和 Dispose 的区别?
- 终结器由 GC 调用,时机不确定,对象至少多存活一次 GC
- Dispose 由开发者调用,确定性释放资源
-
大对象堆(LOH)的特点?
- ≥ 85KB 的对象,等同于 Gen 2 回收,默认不压缩
-
如何减少 GC 压力?
- 对象池、ValueTask、Span<T>、struct、减少分配
练习
- 使用
GC.GetGCMemoryInfo()观察 GC 行为 - 创建大量临时对象,观察 Gen 0/1/2 的回收次数
- 实现完整的 IDisposable 模式
- 使用
ArrayPool<T>优化大数组的分配
评论
登录 后发表评论
暂无评论