一、内存泄漏的典型症状

当你的应用运行时间越长,内存占用越高,甚至最终导致进程崩溃,这时候就该警惕了。比如有个后台服务,刚启动时内存稳定在200MB,运行三天后飙升到2GB,这就是典型的内存泄漏症状。

在.NET Core中,常见表现包括:

  • GC压力增大:通过dotnet-counters工具观察GC Heap Size持续增长
  • 大对象堆碎片化:频繁创建大于85KB的对象导致LOH无法回收
  • 非托管资源泄漏:调用了COM组件或文件句柄未释放
// 示例1:典型的事件订阅泄漏(.NET Core 3.1+)
public class EventPublisher
{
    public static event Action<string> OnDataReceived;

    public static void SendData(string data) => OnDataReceived?.Invoke(data);
}

public class LeakySubscriber : IDisposable
{
    public LeakySubscriber()
    {
        // 订阅事件但未取消订阅
        EventPublisher.OnDataReceived += HandleData;
    }

    private void HandleData(string data)
    {
        Console.WriteLine($"Received: {data}");
    }

    public void Dispose()
    {
        // 应该添加:EventPublisher.OnDataReceived -= HandleData;
    }
}
// 问题:当创建1000个LeakySubscriber实例后,即使调用Dispose(),内存也不会释放

二、系统化排查工具箱

1. 基础诊断工具链

  • Visual Studio诊断工具集:内存快照对比功能
  • dotnet-dump:Linux环境下抓取内存快照
dotnet tool install -g dotnet-dump
dotnet-dump collect -p <PID> --type Full
  • PerfView:分析GC Roots和对象保留路径

2. 关键指标监控

通过Prometheus+Grafana监控这些指标:

// 示例2:暴露GC指标的ASP.NET Core中间件(.NET 6)
app.UseEndpoints(endpoints =>
{
    endpoints.MapMetrics(); // Prometheus-net库
    endpoints.MapGet("/gcstats", async ctx =>
    {
        var gcInfo = GC.GetGCMemoryInfo();
        await ctx.Response.WriteAsJsonAsync(new {
            gcInfo.HeapSizeBytes,
            gcInfo.FragmentedBytes,
            gcInfo.MemoryLoadBytes
        });
    });
});

三、高频泄漏场景实战

1. 缓存失控

// 示例3:错误的静态缓存实践(.NET 7)
public static class CacheService
{
    private static readonly ConcurrentDictionary<string, object> _cache 
        = new ConcurrentDictionary<string, object>();

    public static void AddItem(string key, object value)
    {
        // 危险!没有过期策略
        _cache.TryAdd(key, value);
        
        // 正确做法应使用MemoryCache或IDistributedCache
        // 并设置绝对/滑动过期时间
    }
}
// 泄漏特征:_cache持续增长且永不释放

2. 异步上下文陷阱

// 示例4:Task未正确处理的泄漏(.NET 5+)
public class BackgroundProcessor
{
    private List<Task> _pendingTasks = new List<Task>();

    public void QueueWork(Func<Task> work)
    {
        // 危险!未跟踪的任务可能永远不会完成
        var task = Task.Run(async () => {
            try { await work(); }
            catch { /* 吞掉异常 */ }
        });
        _pendingTasks.Add(task);
    }

    // 正确做法应使用CancellationTokenSource和Task.WhenAll
}
// 泄漏特征:线程池线程和关联对象无法回收

四、根治方案与防御性编程

1. 对象生命周期管理黄金法则

  • IDisposable模式强化版
public class SafeResourceHolder : IDisposable
{
    private bool _disposed;
    private FileStream _fileStream;

    public void OpenFile(string path)
    {
        if (_disposed) throw new ObjectDisposedException(nameof(SafeResourceHolder));
        _fileStream = new FileStream(path, FileMode.Open);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // 释放托管资源
                _fileStream?.Dispose();
            }
            // 释放非托管资源
            _disposed = true;
        }
    }

    ~SafeResourceHolder() => Dispose(false);
}

2. 现代内存管理技巧

  • ArrayPool替代数组分配
// 示例5:高性能缓冲区复用(.NET Core 3.0+)
public async Task ProcessData(Stream input)
{
    var pool = ArrayPool<byte>.Shared;
    byte[] buffer = pool.Rent(81920);
    
    try
    {
        while (await input.ReadAsync(buffer) > 0)
        {
            // 处理数据...
        }
    }
    finally
    {
        pool.Return(buffer);
    }
}
// 优势:避免大对象堆分配和GC压力

五、长效预防机制

  1. CI/CD流水线集成内存测试
# Azure Pipelines示例
- script: |
    dotnet test --collect:"GC API Event Tracing"
    dotnet counters monitor --process-id $(pidof myapp) System.Runtime
  displayName: "内存泄漏检测"
  1. AOP自动跟踪
// 使用PostSharp或Fody实现自动Dispose
[Profile]
public class AutoProfileService : IDisposable
{
    [ProfileMethod]
    public void CriticalOperation() { /*...*/ }
    
    // 编译时自动注入Dispose逻辑
}

通过这套组合拳,我们既能快速定位现存泄漏点,又能建立预防体系。记住,内存管理就像照顾盆栽——定期检查比等它枯死再抢救要省心得多!