.NET 拥有自动内存管理机制——垃圾回收(GC),但这并不意味着开发者可以完全忽视内存问题。高性能应用、游戏、桌面程序和长时间运行的服务都需要深入理解 GC 的工作方式以及非托管资源的管理策略。本文将从 GC 内部机制出发,讲解代龄模型、GC 模式、IDisposable 模式、弱引用以及内存泄漏的排查方法。
GC 的工作原理
.NET GC 是一种分代式、标记-压缩的垃圾回收器。
代龄模型
| 代龄 |
描述 |
回收频率 |
| Gen 0 |
新分配的小对象 |
最频繁,通常几 MB 后触发 |
| Gen 1 |
Gen 0 幸存的对象 |
较少,作为 Gen 0 到 Gen 2 的缓冲区 |
| Gen 2 |
Gen 1 幸存 + 大对象堆 |
最少,整个进程生命周期可能只回收几次 |
1 2 3 4 5
| object obj = new(); int gen = GC.GetGeneration(obj); GC.Collect(0); gen = GC.GetGeneration(obj);
|
大对象堆(LOH)
大于 85,000 字节的对象被分配到大对象堆(Large Object Heap)。LOH 有以下特点:
- 默认不压缩(性能开销大)
- 属于 Gen 2 级别
- 碎片化问题显著
1 2 3 4 5
| byte[] large = new byte[100_000];
GCSettings.LargeObjectHeapCompactionMode = GCSettings.LargeObjectHeapCompactionMode.CompactOnce; GC.Collect();
|
GC 模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| GCSettings.LatencyMode = GCLatencyMode.Interactive;
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
if (GC.TryStartNoGCRegion(100_000_000)) { GC.EndNoGCRegion(); }
|
IDisposable 与 Dispose 模式
非托管资源(文件句柄、网络连接、数据库链接、GDI 对象)不会被 GC 自动回收,需要手动释放。
标准 Dispose 模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class ResourceHolder : IDisposable { private IntPtr _nativeResource; private SafeHandle? _managedResource;
private bool _disposed;
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected virtual void Dispose(bool disposing) { if (_disposed) return;
if (disposing) { _managedResource?.Dispose(); }
if (_nativeResource != IntPtr.Zero) { Marshal.FreeHGlobal(_nativeResource); _nativeResource = IntPtr.Zero; }
_disposed = true; }
~ResourceHolder() { Dispose(false); } }
|
using 声明(C# 8.0+)
1 2 3 4 5 6 7 8 9
| using (var file = new StreamReader("data.txt")) { }
using var reader = new StreamReader("data.txt");
|
SafeHandle 包装
直接使用 IntPtr 携带非托管资源容易漏释放。推荐使用 SafeHandle 派生类:
1 2 3 4 5 6 7 8 9
| public class MySafeHandle : SafeHandleZeroOrMinusOneIsInvalid { public MySafeHandle() : base(true) { }
protected override bool ReleaseHandle() { return NativeMethods.FreeResource(handle) == 0; } }
|
弱引用(WeakReference)
弱引用允许引用一个对象但不阻止 GC 回收它,适用于缓存场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class ExpensiveData { public byte[] Data = new byte[1024 * 1024]; }
var cache = new Dictionary<string, WeakReference<ExpensiveData>>();
void AddToCache(string key, ExpensiveData data) { cache[key] = new WeakReference<ExpensiveData>(data); }
bool TryGetFromCache(string key, out ExpensiveData? data) { if (cache.TryGetValue(key, out var weakRef)) { return weakRef.TryGetTarget(out data); } data = null; return false; }
|
WeakReference<T>(C# 7+)是类型安全的版本。对于小对象,弱引用本身(约 32 字节)的开销可能超过缓存带来的收益,应仅缓存大对象。
内存泄漏常见原因
1. 事件泄漏
1 2 3 4 5 6 7
| public class LeakyPublisher { public event EventHandler? SomethingHappened; }
publisher.SomethingHappened += subscriber.OnSomethingHappened;
|
修复:取消订阅、使用弱事件模式,或改用 IObserver<T>。
2. 静态集合无限增长
1 2 3
| static List<object> _cache = new();
void CacheData(object data) => _cache.Add(data);
|
修复:设置上限、结合弱引用、或使用 MemoryCache(Microsoft.Extensions.Caching)。
3. 匿名方法捕获变量
1 2 3 4 5 6 7 8 9 10
| class Leak { public event Action? OnAction;
public void Subscribe() { var local = new byte[1024 * 1024]; OnAction += () => Console.WriteLine(local.Length); } }
|
4. 未释放的 Timer
1 2 3
| var timer = new Timer(_ => { }, null, 0, 1000); timer.Dispose();
|
内存分析工具
| 工具 |
平台 |
用途 |
| Visual Studio Diagnostic Tools |
Windows |
实时内存/CPU 分析 |
| dotnet-counters |
跨平台 |
实时性能计数器 |
| dotnet-dump |
跨平台 |
离线内存分析(类似 WinDBG) |
| JetBrains dotMemory |
Windows |
深度内存泄漏分析 |
| BenchmarkDotNet |
跨平台 |
精确性能基准 |
1 2 3 4 5 6
| dotnet-counters monitor --process-id 1234 System.Runtime
dotnet-dump collect --process-id 1234 dotnet-dump analyze dump_20250427.dmp
|
减少 GC 压力的编码技巧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| var pool = ArrayPool<byte>.Shared; byte[] buffer = pool.Rent(1024); try { } finally { pool.Return(buffer); }
struct Point { public int X; public int Y; }
var items = Enumerable.Range(1, 100); int threshold = 50; items.Where(i => i > threshold).ToList();
items.Where(i => i > 50).ToList();
|
总结:理解 GC 的分代模型和内存分配策略是编写高性能 .NET 应用的基础。正确实现 Dispose 模式确保非托管资源及时释放,弱引用为缓存场景提供安全方案,而掌握内存分析工具则能在出现问题时快速定位根源。在 .NET 中,”自动内存管理”不等于”无需关心内存”。