You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Redis数据获取后内存未自动释放,仅手动调用GC.Collect()才生效的原因排查

Redis数据获取后内存未自动释放,仅手动调用GC.Collect()才生效的原因排查

我来帮你拆解这个问题,结合你的代码和.NET GC的工作机制,主要原因和可能性大概是这几点:

1. .NET GC的“懒加载”回收特性(最可能的原因)

.NET的垃圾回收器不会在对象刚失去引用就立刻回收,它会等到内存使用量达到某个阈值(不同代的阈值不同)时,才会自动触发回收。

你的代码里,每次调用GetAllMonstersSetAsync()都会创建一个ConcurrentDictionary,还有Redis返回的HashEntry数组。这些对象虽然已经失去了根引用(你没保存方法返回值,循环里也没持有),但只要当前内存还没触发GC的自动回收条件,它们就会暂时留在内存里,看起来像是“内存泄漏”。而手动调用GC.Collect()是强制触发全量垃圾回收,直接把这些可回收对象清理掉,所以内存会立刻下降。

这种情况其实是.NET GC的正常行为,不是真的内存泄漏——只要你持续运行,当内存压力足够大时,GC最终还是会自动回收这些内存,只是不会像手动调用那样“立竿见影”。

2. 大对象堆(LOH)的回收频率问题

如果你的ConcurrentDictionary每次都包含大量键值对(比如几万甚至更多),这个对象会被分配到**大对象堆(LOH)**里。.NET默认对LOH的回收频率很低:只有在触发Full GC的时候才会回收LOH,而Full GC的触发条件比普通GC严格得多(比如小对象堆内存不足、系统内存不足等)。

这就导致这些大的字典对象会在LOH里“待很久”,内存持续上涨,直到Full GC自动触发或者你手动调用GC.Collect()(手动调用会强制触发Full GC,顺带回收LOH)。

3. 有没有可能存在隐性引用?

你已经排查了“没有保存返回值引用”,这点做得很好,但可以再确认两个点:

  • 检查StackExchange.Redis的客户端是否有隐性持有HashEntry数组的情况?不过你已经复用了单例的Redis多路复用器,这是正确的用法,一般不会有这个问题;
  • 有没有可能ConcurrentDictionary的内部结构被某些异步状态机意外持有?不过你用了await _ = await...的方式尝试过,这个可能性也很低。

怎么验证和优化?

验证是不是GC时机问题

用Visual Studio的诊断工具或者第三方工具(比如dotMemory)拍两次内存快照:

  1. 内存上涨后,拍第一次快照;
  2. 手动调用GC.Collect()后,拍第二次快照。
    对比两次快照里ConcurrentDictionary和HashEntry对象的数量,如果第一次有大量这类对象,第二次几乎没有,而且这些对象在第一次快照里显示“没有根引用”,那就能确定是GC自动触发时机的问题,不是泄漏。

优化建议

  1. 复用对象,减少大对象分配:把ConcurrentDictionary改成类的字段,每次调用GetAllMonstersSetAsync()时先清空字典,再重新填充,而不是每次new一个新的。这样能避免频繁在LOH里分配大对象,减少内存波动:
// 把字典作为类的字段
private readonly ConcurrentDictionary<long, string> _monsterSetData = new();

private async Task GetAllMonstersSetAsync() {
    var redis = _redis.GetDatabase();
    _monsterSetData.Clear(); // 先清空旧数据
    var monsterActiveSpawns = await redis.HashGetAllAsync("Game:Monster:Spawns:Status:Active");
    var monsterInactiveSpawns = await redis.HashGetAllAsync("Game:Monster:Spawns:Status:Inactive");
    
    foreach (var monsterSpawn in monsterActiveSpawns) {
        _monsterSetData.TryAdd((long)monsterSpawn.Name, monsterSpawn.Value!);
    }
    foreach (var monsterSpawn in monsterInactiveSpawns) {
        _monsterSetData.TryAdd((long)monsterSpawn.Name, monsterSpawn.Value!);
    }
}
  1. 调整GC配置(谨慎操作):如果确实是LOH回收不及时,可以通过配置让.NET更频繁地回收LOH,比如在appsettings.json里添加:
{
  "GC": {
    "LOHCompactionMode": "CompactOnce"
  }
}

不过这个建议谨慎使用,因为会增加GC的开销,优先考虑复用对象的方案。

总结

你遇到的情况大概率是GC自动触发时机的正常表现,不是真的内存泄漏。手动GC.Collect()能解决是因为强制触发了全量回收,包括LOH的内存。如果想优化内存波动,优先考虑复用大对象,减少LOH的分配。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 10:18:04