.NET 中常见的内存泄漏场景及解决方案
尽管 .NET 拥有自动垃圾回收(GC),但内存泄漏仍然可能发生。以下是常见场景及解决方案:
1. 事件处理程序未注销
场景:
csharp
public class Publisher
{
public event EventHandler SomethingHappened;
}
public class Subscriber
{
public Subscriber(Publisher publisher)
{
// 订阅事件
publisher.SomethingHappened += OnSomethingHappened;
}
private void OnSomethingHappened(object sender, EventArgs e) { }
}
// Subscriber 不再需要时,仍被 Publisher 引用
解决方案:
csharp
// 1. 实现 IDisposable 并取消订阅
public class Subscriber : IDisposable
{
private Publisher _publisher;
public Subscriber(Publisher publisher)
{
_publisher = publisher;
publisher.SomethingHappened += OnSomethingHappened;
}
public void Dispose()
{
_publisher.SomethingHappened -= OnSomethingHappened;
}
}
// 2. 使用弱事件模式(WeakEventManager)
WeakEventManager<Publisher, EventArgs>.AddHandler(
publisher,
nameof(Publisher.SomethingHappened),
OnSomethingHappened);
2. 静态集合无限增长
场景:
csharp
public static class Cache
{
private static List<object> _cache = new List<object>();
public static void Add(object item)
{
_cache.Add(item); // 永远不会被清除
}
}
解决方案:
csharp
// 1. 使用 WeakReference
public static class Cache
{
private static List<WeakReference<object>> _cache = new List<WeakReference<object>>();
public static void Add(object item)
{
_cache.Add(new WeakReference<object>(item));
}
}
// 2. 使用 MemoryCache(带过期策略)
private static MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
public static void Add(string key, object item, TimeSpan expiration)
{
_cache.Set(key, item, expiration);
}
// 3. 定期清理
private static ConcurrentDictionary<string, DateTime> _accessTimes = new ConcurrentDictionary<string, DateTime>();
public static void Cleanup()
{
var expired = _accessTimes.Where(kvp =>
DateTime.Now - kvp.Value > TimeSpan.FromHours(1));
// 移除过期项
}
3. 非托管资源未释放
场景:
csharp
public class ResourceHolder
{
private IntPtr _unmanagedHandle;
public ResourceHolder()
{
_unmanagedHandle = AllocateUnmanagedResource();
}
// 缺少 Dispose/Finalizer
}
解决方案:
csharp
public class ResourceHolder : IDisposable
{
private IntPtr _unmanagedHandle;
private bool _disposed = false;
public ResourceHolder()
{
_unmanagedHandle = AllocateUnmanagedResource();
}
// 析构函数(最后的安全网)
~ResourceHolder()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (_unmanagedHandle != IntPtr.Zero)
{
FreeUnmanagedResource(_unmanagedHandle);
_unmanagedHandle = IntPtr.Zero;
}
_disposed = true;
}
}
}
// 使用 using 语句确保释放
using (var resource = new ResourceHolder())
{
// 使用资源
}
4. 线程相关泄漏
场景:
csharp
public class Worker
{
private Timer _timer;
public void Start()
{
_timer = new Timer(Callback, null, 0, 1000);
}
private void Callback(object state)
{
// 持有外部对象引用
}
// Timer 持有 Worker 引用,阻止垃圾回收
}
解决方案:
csharp
// 1. 停止 Timer
public void Stop()
{
_timer?.Dispose();
_timer = null;
}
// 2. 使用 CancellationToken
public class Worker : IDisposable
{
private CancellationTokenSource _cts = new CancellationTokenSource();
private Task _task;
public void Start()
{
_task = Task.Run(async () =>
{
while (!_cts.Token.IsCancellationRequested)
{
// 工作逻辑
await Task.Delay(1000, _cts.Token);
}
});
}
public void Dispose()
{
_cts?.Cancel();
_task?.Wait();
_cts?.Dispose();
}
}
5. WPF/Silverlight 特定问题
场景:
csharp
// 数据绑定泄漏
public class ViewModel
{
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();
}
// View 订阅了 CollectionChanged,但 View 关闭后未取消订阅
解决方案:
csharp
// 1. 使用 WeakEventManager
WeakEventManager<ObservableCollection<Item>, NotifyCollectionChangedEventArgs>
.AddHandler(collection, "CollectionChanged", OnCollectionChanged);
// 2. 手动清理
public class MyView : UserControl
{
protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);
// 清理绑定和事件
BindingOperations.ClearAllBindings(this);
}
}
// 3. 使用 x:Reference 时小心
<!-- 避免循环引用 -->
<DataTemplate DataType="{x:Type local:Item}">
<!-- 不要引用可能包含自己的父对象 -->
</DataTemplate>
6. 闭包捕获外部变量
场景:
csharp
public class Service
{
private List<Action> _callbacks = new List<Action>();
public void RegisterCallback(Action callback)
{
_callbacks.Add(callback);
}
}
// 使用闭包
var bigObject = new BigObject();
service.RegisterCallback(() =>
{
// 捕获 bigObject,阻止其被回收
Console.WriteLine(bigObject.Name);
});
解决方案:
csharp
// 1. 避免捕获大对象
string name = bigObject.Name; // 只捕获需要的值
service.RegisterCallback(() => Console.WriteLine(name));
// 2. 使用弱引用
var weakRef = new WeakReference<BigObject>(bigObject);
service.RegisterCallback(() =>
{
if (weakRef.TryGetTarget(out var obj))
{
Console.WriteLine(obj.Name);
}
});
// 3. 提供取消注册机制
public class Service
{
private List<WeakReference<Action>> _callbacks = new List<WeakReference<Action>>();
public IDisposable RegisterCallback(Action callback)
{
var weakRef = new WeakReference<Action>(callback);
_callbacks.Add(weakRef);
return new Unsubscriber(() => _callbacks.Remove(weakRef));
}
}
7. 大对象堆(LOH)碎片化
场景:
csharp
// 频繁分配和释放大对象(>85KB)
byte[] buffer = new byte[100000]; // 分配到LOH
// 频繁操作导致LOH碎片
解决方案:
csharp
// 1. 使用对象池
public class BufferPool
{
private static ConcurrentQueue<byte[]> _pool = new ConcurrentQueue<byte[]>();
public static byte[] Rent(int size)
{
if (_pool.TryDequeue(out var buffer) && buffer.Length >= size)
return buffer;
return new byte[size];
}
public static void Return(byte[] buffer)
{
_pool.Enqueue(buffer);
}
}
// 2. 使用 ArrayPool(.NET Core+)
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(100000);
// 使用 buffer
pool.Return(buffer);
// 3. 避免频繁分配大对象
8. 诊断工具和技术
csharp
// 1. 使用性能计数器
// - .NET CLR Memory: % Time in GC
// - .NET CLR Memory: # Bytes in all Heaps
// 2. 代码分析
[Conditional("DEBUG")]
public static void CheckMemory()
{
var process = Process.GetCurrentProcess();
var memoryMB = process.WorkingSet64 / 1024 / 1024;
if (memoryMB > 500) // 阈值
{
Debug.WriteLine($"内存使用过高: {memoryMB}MB");
// 触发GC分析
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
// 3. 使用分析器
// - Visual Studio Diagnostic Tools
// - dotMemory (JetBrains)
// - PerfView
// - .NET Memory Profiler
最佳实践总结
- 及时释放资源 :实现
IDisposable模式,使用using语句 - 小心事件订阅 :总是配对
+=和-=,考虑弱事件 - 管理集合生命周期:避免静态集合无限增长
- 监控大对象分配:使用对象池,避免LOH碎片
- 定期代码审查:检查是否有循环引用、未释放资源
- 使用诊断工具:定期进行内存分析
- 自动化测试:编写内存泄漏检测测试
- 升级到 .NET Core/5+:新的GC改进(容器感知、区域等)
示例:内存泄漏检测模式
csharp
public class LeakDetector<T> where T : class
{
private readonly WeakReference<T> _weakRef;
public LeakDetector(T obj)
{
_weakRef = new WeakReference<T>(obj);
}
~LeakDetector()
{
if (_weakRef.TryGetTarget(out _))
{
// 对象仍然存活,可能泄漏
Debug.WriteLine($"警告: {typeof(T).Name} 可能泄漏!");
#if DEBUG
// 触发GC并再次检查
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
if (_weakRef.TryGetTarget(out _))
{
Debug.WriteLine($"确认: {typeof(T).Name} 已泄漏!");
}
#endif
}
}
}
// 使用
public class MyClass
{
private static LeakDetector<MyClass> _detector;
public MyClass()
{
_detector = new LeakDetector<MyClass>(this);
}
}
通过理解这些常见场景并采用相应的预防措施,可以显著减少.NET应用程序中的内存泄漏问题。