C# Task执行完成未销毁持续占内存,手动清理列表无效该如何解决?
问题原因与解决方案
为什么Task执行完成后内存没有被回收
- .NET的垃圾回收是按需触发的,不是对象失去引用后就立刻执行回收,只有当前进程内存占用达到GC阈值、或者代码显式触发回收时才会执行,你观测到的内存未释放,首先大概率是GC还没被触发。
- 你代码里产生的都是大对象:单个int占4字节,10亿个int总大小达4GB,加上List扩容过程中产生的多份中间数组,所有超过85000字节的对象都会被分配到大对象堆(LOH)。在.NET Framework以及早期.NET Core版本中,LOH默认不会自动压缩,且回收阈值远高于小对象堆,就算对象已经没有引用,也会因为内存碎片、阈值未达到等原因,在任务管理器中仍然显示为程序占用的内存。
- async方法的状态机特性会延长局部变量的生命周期:
Start是异步方法,局部变量values会被封装到异步状态机类中,状态机本身的回收时机比普通同步方法的局部变量更晚,就算你手动把values赋值为null,只要状态机还没被GC标记为可回收,对应的数组内存也不会被释放。 - 你观测到“启动新Task才会释放内存”的现象,本质是新Task申请内存的动作触发了GC的阈值校验,此时才会执行回收动作清理之前的无效对象,所以会出现内存先降后升的表现。
为什么列表不是临时存储而是持续占用内存
除了上述GC非即时触发、LOH特性、异步状态机延长生命周期的原因外,如果你是在调试模式下运行程序,JIT编译器会出于调试需要,将所有局部变量的生命周期延长到方法执行结束,哪怕变量已经不会再被使用、甚至被手动赋值为null,GC也不会提前回收这部分内存,进一步加剧了内存持续占用的表现。
当前操作存在的问题与优化方案
- 未提前指定List容量:List默认的扩容逻辑是每次容量翻倍,你往空List里加10亿个元素的过程中,会产生多个大小逐步翻倍的临时数组,这些数组全部会进入LOH,额外占用数GB的无效内存。如果提前知道元素总数,初始化时指定容量即可避免扩容开销:
List<int> values = new List<int>(1000000000); - 大对象没有配合手动回收逻辑:如果确实需要及时释放LOH内存,在确认列表不再使用后,可以显式触发GC并开启LOH压缩(适用于.NET Core 3.0+版本):
// 手动触发LOH压缩 GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect(); GC.WaitForPendingFinalizers();
注意不要频繁调用上述代码,否则会严重影响程序性能。
- 不合理的全量内存存储:处理10亿级别的数据时,优先选择流式处理方案,边读取边处理,不要一次性全量加载到内存中,从根源上避免超大内存占用。
内容的提问来源于stack exchange,提问作者Zelos
相关产品推荐
相关产品推荐

