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)拍两次内存快照:
- 内存上涨后,拍第一次快照;
- 手动调用
GC.Collect()后,拍第二次快照。
对比两次快照里ConcurrentDictionary和HashEntry对象的数量,如果第一次有大量这类对象,第二次几乎没有,而且这些对象在第一次快照里显示“没有根引用”,那就能确定是GC自动触发时机的问题,不是泄漏。
优化建议
- 复用对象,减少大对象分配:把
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!); } }
- 调整GC配置(谨慎操作):如果确实是LOH回收不及时,可以通过配置让.NET更频繁地回收LOH,比如在appsettings.json里添加:
{ "GC": { "LOHCompactionMode": "CompactOnce" } }
不过这个建议谨慎使用,因为会增加GC的开销,优先考虑复用对象的方案。
总结
你遇到的情况大概率是GC自动触发时机的正常表现,不是真的内存泄漏。手动GC.Collect()能解决是因为强制触发了全量回收,包括LOH的内存。如果想优化内存波动,优先考虑复用大对象,减少LOH的分配。
内容来源于stack exchange

