一、内存泄漏的那些事儿

内存泄漏就像家里漏水的水龙头,虽然每次只漏一滴,但时间长了能把整个房子淹了。在开发中,哪怕是一个小对象没释放,运行几个月后也可能让服务器崩溃。

典型场景

  • 静态集合长期持有对象引用
  • 未注销的事件监听
  • 未释放的IDisposable资源(如文件流、数据库连接)

举个实际案例(技术栈:.NET Core 6 + C#):

public class CacheService
{
    // 危险操作:静态字典会一直增长,导致内存泄漏
    private static readonly Dictionary<int, byte[]> _cache = new();

    public void AddData(int id, byte[] data)
    {
        _cache[id] = data; // 数据永远无法被GC回收
    }
}
// 正确做法:使用WeakReference或定时清理
public class SafeCache
{
    private static readonly Dictionary<int, WeakReference<byte[]>> _cache = new();
    
    public void AddData(int id, byte[] data)
    {
        _cache[id] = new WeakReference<byte[]>(data);
    }
}

二、诊断工具三剑客

工欲善其事必先利其器,这三个工具能帮你快速定位问题:

1. Visual Studio诊断工具

在Debug时点击【分析】→【内存性能分析】,可以拍摄堆快照对比对象增长情况。适合开发阶段使用。

2. dotnet-dump

生产环境救星!通过以下命令抓取内存快照:

dotnet tool install -g dotnet-dump
dotnet-dump collect -p <PID> 

分析命令示例:

dotnet-dump analyze dumpfile.dmp
> dumpheap -stat  # 统计对象数量
> gcroot <object_address>  # 查找泄漏对象的引用链

3. PerfView

微软官方性能工具,可以分析内存分配热点:

1. 运行PerfView → 选择【Memory】→【Take Heap Snapshot】
2. 对比两次快照的【Diff】视图
3. 查看【GC Heap Net Mem】指标变化

三、高频泄漏场景实战

场景1:事件订阅未解除

public class EventPublisher
{
    public event Action OnDataProcessed;
}

public class Subscriber
{
    public void Subscribe(EventPublisher publisher)
    {
        publisher.OnDataProcessed += HandleEvent; // 订阅后未取消
    }

    private void HandleEvent() { /*...*/ }
}

// 修复方案:实现IDisposable
public class SafeSubscriber : IDisposable
{
    private EventPublisher _publisher;
    
    public void Subscribe(EventPublisher publisher) 
    {
        _publisher = publisher;
        publisher.OnDataProcessed += HandleEvent;
    }

    public void Dispose() 
    {
        _publisher.OnDataProcessed -= HandleEvent; // 显式解除
    }
}

场景2:Timer未释放

// 错误示例:Timer会保持实例存活
public class BackgroundWorker
{
    private Timer _timer = new Timer(_ => DoWork(), null, 0, 1000);
    
    private void DoWork() { /*...*/ }
}

// 正确做法:继承IDisposable
public class SafeWorker : IDisposable
{
    private readonly Timer _timer;
    private bool _disposed;

    public SafeWorker() 
    {
        _timer = new Timer(_ => DoWork(), null, 0, 1000);
    }

    public void Dispose()
    {
        _timer?.Dispose();
        _disposed = true;
    }
}

四、防御性编程技巧

  1. using语句:自动调用Dispose()

    using var stream = new FileStream("data.txt", FileMode.Open);
    // 代码块结束时自动释放
    
  2. 对象池模式:复用昂贵对象

    public class DbConnectionPool
    {
        private ConcurrentQueue<SqlConnection> _pool = new();
    
        public SqlConnection Rent()
        {
            if (_pool.TryDequeue(out var conn)) 
                return conn;
    
            return new SqlConnection("ConnectionString");
        }
    
        public void Return(SqlConnection conn) 
        {
            _pool.Enqueue(conn);
        }
    }
    
  3. 弱引用模式

    var weakRef = new WeakReference<BigObject>(new BigObject());
    if (weakRef.TryGetTarget(out var target))
    {
        // 当内存不足时target可能为null
    }
    

五、性能与资源的平衡

内存优化不是越极端越好,需要权衡:

策略 优点 缺点
对象池 减少GC压力 增加代码复杂度
弱引用 避免强引用 需处理对象失效
定时清理 控制内存上限 可能引发性能波动

黄金法则

  • Web应用中,单个请求完成后应释放所有临时对象
  • 后台服务要明确生命周期管理
  • 缓存数据必须设置过期策略

六、总结与最佳实践

  1. 预防胜于治疗:在代码审查阶段检查常见陷阱
  2. 监控生产环境:配置内存阈值告警
  3. 渐进式修复:优先解决最大的泄漏点

最后记住:内存管理就像打理花园,定期除草(释放资源)才能让应用健康生长。