.NET 中常见的内存泄漏场景及解决方案

.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

最佳实践总结

  1. 及时释放资源 :实现 IDisposable 模式,使用 using 语句
  2. 小心事件订阅 :总是配对 +=-=,考虑弱事件
  3. 管理集合生命周期:避免静态集合无限增长
  4. 监控大对象分配:使用对象池,避免LOH碎片
  5. 定期代码审查:检查是否有循环引用、未释放资源
  6. 使用诊断工具:定期进行内存分析
  7. 自动化测试:编写内存泄漏检测测试
  8. 升级到 .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应用程序中的内存泄漏问题。

相关推荐
专注VB编程开发20年12 小时前
C#全面超越JAVA,主要还是跨平台用的人少
java·c#·.net·跨平台
一个帅气昵称啊1 天前
.Net通过EFCore和仓储模式实现统一数据权限管控并且相关权限配置动态生成
.net·efcore·仓储模式
helloworddm1 天前
CalculateGrainDirectoryPartition
服务器·c#·.net
步步为营DotNet1 天前
深度剖析.NET中HttpClient的请求重试机制:可靠性提升与实践优化
开发语言·php·.net
ChaITSimpleLove1 天前
使用 .net10 构建 AI 友好的 RSS 订阅机器人
人工智能·.net·mcp·ai bot·rss bot
专注VB编程开发20年1 天前
vb.net宿主程序通过统一接口直接调用,命名空间要一致
服务器·前端·.net
ChaITSimpleLove2 天前
基于 .NET Garnet 1.0.91 实现高性能分布式锁(使用 Lua 脚本)
分布式·.net·lua
用户4488466710602 天前
.NET进阶——深入理解线程(2)Thread入门到精通
c#·.net