一、内存泄漏的那些事儿

做.NET Core开发的朋友们肯定都遇到过这种情况:应用跑着跑着突然变卡,服务器内存蹭蹭往上涨,最后直接崩溃。这种"内存只进不出"的现象,就是我们今天要聊的内存泄漏。

举个生活中的例子,就像你家的水龙头没关紧,水池早晚会被灌满。代码里的对象如果只创建不释放,内存这个"水池"也会溢出。下面我们用实际代码演示一个典型场景(技术栈:.NET Core 6 + C#):

public class LeakyService
{
    private static List<byte[]> _cache = new List<byte[]>();
    
    public void ProcessRequest()
    {
        // 每次处理请求都往静态集合添加1MB数据
        _cache.Add(new byte[1024 * 1024]); 
        
        /* 问题分析:
           1. 静态集合生命周期与应用相同
           2. 添加的元素永远不会被移除
           3. 随着请求量增加必然OOM */
    }
}

二、揪出内存泄漏的元凶

要解决问题得先找到问题,.NET Core提供了强大的诊断工具。推荐使用dotnet-dump+Visual Studio组合拳:

  1. 首先在生产环境捕获内存快照:
dotnet-dump collect -p <PID> --type heap
  1. 然后用VS分析dump文件,重点看:
  • 对象存活路径(GC Roots)
  • 大对象堆(LOH)状态
  • 字符串驻留情况

这里有个实际案例:某电商系统促销时内存暴涨,通过分析发现是Redis连接池泄漏:

public class RedisClient
{
    private static ConcurrentBag<ConnectionMultiplexer> _connections 
        = new ConcurrentBag<ConnectionMultiplexer>();
    
    public ConnectionMultiplexer GetConnection()
    {
        if(!_connections.TryTake(out var conn)){
            conn = ConnectionMultiplexer.Connect("localhost");
        }
        return conn;
        
        /* 致命缺陷:
           1. 借出的连接没有归还机制
           2. 连接对象包含非托管资源
           3. 最终导致端口耗尽 */
    }
}

三、常见泄漏场景及修复方案

根据笔者经验,.NET Core内存泄漏主要集中在以下几个场景:

1. 事件订阅未注销

public class EventPublisher
{
    public static event Action OnDataProcessed;
}

public class Subscriber : IDisposable
{
    public Subscriber() {
        EventPublisher.OnDataProcessed += HandleEvent;
    }
    
    private void HandleEvent() { /*...*/ }
    
    public void Dispose() {
        // 必须显式取消订阅!!!
        EventPublisher.OnDataProcessed -= HandleEvent;
    }
}

2. 缓存无限增长

推荐使用MemoryCache并设置过期策略:

var cache = new MemoryCache(new MemoryCacheOptions());
cache.Set("key", bigData, 
    new MemoryCacheEntryOptions()
        .SetSlidingExpiration(TimeSpan.FromMinutes(10))
        .RegisterPostEvictionCallback((k,v,r,s) => {
            /* 缓存淘汰时的清理逻辑 */ }));

3. 异步方法中的闭包

public async Task LeakyMethod()
{
    var heavyObject = new byte[1000000];
    
    // 错误写法:闭包捕获了heavyObject
    Task.Run(() => Console.WriteLine(heavyObject.Length));
    
    /* 正确做法:
       1. 避免在异步中捕获大对象
       2. 或者显式置为null */
    await Task.Yield();
    heavyObject = null;
}

四、防患于未然的实践建议

经过多年踩坑总结,推荐这些最佳实践:

  1. 代码规范
  • 所有实现IDisposable的类必须using
  • 静态集合必须设计清理机制
  • 避免在静态字段中保存实例对象
  1. 监控方案
// 在Startup.cs中添加内存监控
services.AddHealthChecks()
    .AddProcessAllocatedMemoryHealthCheck(maxMegabytes: 1024);
  1. 压测技巧: 使用BenchmarkDotNet模拟长期运行:
[MemoryDiagnoser]
public class MemoryBenchmark
{
    [Benchmark]
    public void TestLeakScenario()
    {
        // 模拟持续运行1小时
        for(int i=0; i<3600; i++){
            new LeakyService().ProcessRequest();
        }
    }
}

五、终极解决方案:GC调优

当常规手段无效时,可以考虑调整GC策略。在项目文件中添加这些参数:

<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
  <ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
  <GCHeapCount>4</GCHeapCount> <!-- 根据CPU核心数调整 -->
</PropertyGroup>

但要注意:GC调优是把双刃剑,不当配置可能导致更严重的卡顿。建议先在测试环境验证。

总结

内存泄漏就像程序员的慢性病,初期症状不明显,但积累到一定量就会致命。通过本文介绍的工具和方法,相信大家已经掌握了诊断和治疗的"医术"。记住关键点:预防胜于治疗,规范优于技巧。

最后送大家一个检查清单:

  1. 静态字段是否引用了可变数据?
  2. 事件订阅是否有注销机制?
  3. 缓存是否有淘汰策略?
  4. 非托管资源是否正确释放?
  5. 是否有长期存活的大对象?