Dispose 不释放?C# 资源泄漏的 3 种隐蔽场景排查

张开发
2026/4/17 20:58:46 15 分钟阅读

分享文章

Dispose 不释放?C# 资源泄漏的 3 种隐蔽场景排查
大家好我是码农刚子。最近在做项目代码审查时发现了一个有意思的现象大家都知道要用using或Dispose()来释放资源但真正遇到资源泄漏时还是一脸懵。有人问我刚哥我都调用Dispose()了为什么内存还在涨说实话这个问题问得好。因为Dispose 不释放的坑远比你想象的要深。今天我就从 6 年 .NET 开发的经验出发给你揭露 3 种最隐蔽、最容易踩的资源泄漏场景。场景 1异常中断导致 Dispose 永不执行这是最常见的坑。很多人写代码时脑子里想的是正常流程但忽略了异常这个幽灵。问题代码public class ResourceLeakDemo{ public void BadExample() { SqlConnection conn new SqlConnection(Serverlocalhost;Databasetest); conn.Open(); // 如果这里抛异常conn 永远不会被释放 var result ExecuteQuery(conn); conn.Dispose(); // 这行代码可能永远执行不到 } private object ExecuteQuery(SqlConnection conn) { throw new Exception(模拟查询异常); }}问题分析如果ExecuteQuery()抛异常程序直接跳到 catch 块或调用者conn.Dispose()这一行永远不会执行连接对象留在内存中等待 GC 回收但 GC 不一定及时正确做法// 方案 1using 语句推荐public void GoodExample_Using(){ using (SqlConnection conn new SqlConnection(Serverlocalhost;Databasetest)) { conn.Open(); var result ExecuteQuery(conn); // 即使异常using 也会自动调用 Dispose() }} // 方案 2using 声明C# 8.0更简洁public void GoodExample_UsingDeclaration(){ using SqlConnection conn new SqlConnection(Serverlocalhost;Databasetest); conn.Open(); var result ExecuteQuery(conn); // 方法结束时自动 Dispose()} // 方案 3try-finally不推荐但有时必要public void GoodExample_TryFinally(){ SqlConnection conn new SqlConnection(Serverlocalhost;Databasetest); try { conn.Open(); var result ExecuteQuery(conn); } finally { conn?.Dispose(); // 无论如何都会执行 }}关键点using 语句会在 IL 层面生成 try-finally保证 Dispose 一定执行C# 8.0 的using声明更简洁自动在作用域结束时释放永远不要依赖手动调用 Dispose异常会破坏你的计划场景 2事件订阅导致的隐形引用链这个坑特别隐蔽因为代码看起来完全没问题但内存就是不释放。问题代码public class EventLeakDemo{ public class DataService { public event EventHandler OnDataChanged; public void NotifyDataChanged() { OnDataChanged?.Invoke(this, EventArgs.Empty); } } public class UIComponent { private DataService _service; public UIComponent(DataService service) { _service service; // 订阅事件但从不取消订阅 _service.OnDataChanged OnServiceDataChanged; } private void OnServiceDataChanged(object sender, EventArgs e) { Console.WriteLine(数据已更新); } } public void LeakyCode() { var service new DataService(); var ui new UIComponent(service); // ui 对象即使不再使用也不会被 GC 回收 // 因为 service 的 OnDataChanged 事件持有对 ui 的引用 ui null; // 这行代码不会释放 ui }}问题分析UIComponent 订阅了DataService的事件事件处理器OnServiceDataChanged是实例方法隐含持有this的引用即使ui nullservice.OnDataChanged的委托链中仍然持有对ui的引用只要service还活着ui就永远不会被 GC 回收正确做法public class EventLeakFixed{ public class DataService : IDisposable { public event EventHandler OnDataChanged; public void NotifyDataChanged() { OnDataChanged?.Invoke(this, EventArgs.Empty); } public void Dispose() { // 清空所有事件订阅 OnDataChanged null; } } public class UIComponent : IDisposable { private DataService _service; public UIComponent(DataService service) { _service service; _service.OnDataChanged OnServiceDataChanged; } private void OnServiceDataChanged(object sender, EventArgs e) { Console.WriteLine(数据已更新); } public void Dispose() { // 关键取消事件订阅 if (_service ! null) { _service.OnDataChanged - OnServiceDataChanged; } } } public void CorrectCode() { var service new DataService(); using (var ui new UIComponent(service)) { // 使用 ui } // 自动调用 ui.Dispose()取消事件订阅 using (service) { // 使用 service } // 自动调用 service.Dispose()清空事件 }}关键点订阅事件时一定要在适当时机取消订阅如果对象实现了IDisposable在 Dispose 中取消所有事件订阅使用弱事件模式Weak Event Pattern可以避免这个问题在 WPF/MVVM 框架中这个坑特别常见场景 3静态引用和单例模式中的隐形泄漏这个坑最狡猾因为静态对象的生命周期是整个应用程序很容易被忽视。问题代码public class SingletonLeakDemo{ // 单例模式 public class CacheManager { private static CacheManager _instance new CacheManager(); private Dictionarystring, IDisposable _resources new(); public static CacheManager Instance _instance; public void AddResource(string key, IDisposable resource) { _resources[key] resource; } public void RemoveResource(string key) { // 问题只是从字典中移除但没有释放资源 _resources.Remove(key); } } public void LeakyCode() { // 创建一个需要释放的资源 var conn new SqlConnection(Serverlocalhost;Databasetest); // 添加到单例缓存 CacheManager.Instance.AddResource(conn1, conn); // 后来想移除这个资源 CacheManager.Instance.RemoveResource(conn1); // 问题conn 对象虽然从字典中移除了但从未被 Dispose() // 而且 CacheManager 是静态的整个应用生命周期都存在 // 所以 conn 永远不会被 GC 回收 }}问题分析单例对象的生命周期 应用程序生命周期如果单例中存储了需要释放的资源这些资源也会被永久保留即使从字典中移除如果没有显式 Dispose资源仍然泄漏正确做法public class SingletonLeakFixed{ public class CacheManager : IDisposable { private static readonly LazyCacheManager _instance new LazyCacheManager(() new CacheManager()); private Dictionarystring, IDisposable _resources new(); private bool _disposed false; public static CacheManager Instance _instance.Value; public void AddResource(string key, IDisposable resource) { if (_disposed) throw new ObjectDisposedException(nameof(CacheManager)); _resources[key] resource; } public void RemoveResource(string key) { if (_resources.TryGetValue(key, out var resource)) { // 关键移除时立即释放资源 resource?.Dispose(); _resources.Remove(key); } } public void Dispose() { if (_disposed) return; // 释放所有缓存的资源 foreach (var resource in _resources.Values) { resource?.Dispose(); } _resources.Clear(); _disposed true; } } public void CorrectCode() { var conn new SqlConnection(Serverlocalhost;Databasetest); CacheManager.Instance.AddResource(conn1, conn); // 移除时自动释放 CacheManager.Instance.RemoveResource(conn1); // 应用关闭时释放所有资源 CacheManager.Instance.Dispose(); }}关键点单例对象也要实现IDisposable在移除资源时立即调用Dispose()应用关闭时显式调用单例的Dispose()方法使用LazyT实现线程安全的单例排查技巧如何发现资源泄漏1. 使用内存分析工具// 在 Visual Studio 中使用内存分析工具// Debug → Performance Profiler → Memory Usage// 对比堆快照找出未释放的对象 public void MemoryLeakTest(){ for (int i 0; i 10000; i) { var conn new SqlConnection(Serverlocalhost;Databasetest); conn.Open(); // 忘记 Dispose } // 内存分析工具会显示 10000 个 SqlConnection 对象未释放}2. 使用 GC.GetTotalMemory() 监控public void MonitorMemory(){ long before GC.GetTotalMemory(true); // 执行可能泄漏的代码 for (int i 0; i 1000; i) { using (var conn new SqlConnection(Serverlocalhost;Databasetest)) { conn.Open(); } } long after GC.GetTotalMemory(true); Console.WriteLine($内存增长: {(after - before) / 1024 / 1024} MB); // 如果增长过大说明有泄漏}3. 使用 Finalizer 检测public class ResourceWithFinalizer : IDisposable{ private bool _disposed false; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { // 释放托管资源 } _disposed true; } } ~ResourceWithFinalizer() { // 如果这个 Finalizer 被调用说明 Dispose 没有被正确调用 Console.WriteLine(警告对象通过 Finalizer 被回收可能存在泄漏); Dispose(false); }}总结资源泄漏的 3 种隐蔽场景场景原因解决方案异常中断异常导致 Dispose 代码不执行使用using或 try-finally事件订阅事件处理器持有对象引用取消订阅或使用弱事件模式静态引用单例/静态对象生命周期过长在移除时立即 Dispose应用关闭时清理最后的建议永远使用using语句不要手动调用 Dispose订阅事件时一定要记得取消订阅单例对象也要实现 IDisposable并在适当时机释放定期用内存分析工具检查不要等到线上才发现下次面试被问到如何排查资源泄漏你就可以从这 3 个场景入手展示出你对 .NET 内存管理的深刻理解。你在项目中遇到过资源泄漏吗欢迎在评论区分享你的踩坑故事#csharp #Dispose #资源泄露 #避坑

更多文章