ASP.NET MVC应用内存无法释放问题:调用控制器方法后内存持续攀升
我正在开发一个ASP.NET MVC应用,主页面设有按钮用于调用控制器方法,初始内存占用为137MB。调用该方法时,仅通过for循环向列表中添加测试数据,相关代码如下:
public async Task<IActionResult> Search() { List<SearchResultViewModel>? lstSearch = new(); lstSearch = ParticipantSearch(); GC.Collect(); GC.WaitForPendingFinalizers(); return PartialView("Search"); } private List<SearchResultViewModel>? ParticipantSearch() { List<SearchResultViewModel>? lstSearch = new(); for (int i= 1; i< 100000; i++) { SearchResultViewModel search = new(); search.ParticipantId = i; search.Relationship = "asd"; search.CobraStatus = "asd"; search.QualifyingEventType = "asd"; search.FirstName = "asd"; search.LastName = "asd"; search.SocialSecurityNumber = "asd"; search.ParticipantStatus = "asd"; lstSearch.Add(search); } return lstSearch; }
调用方法后内存占用升至174MB,向页面返回null后内存仍维持在174MB;再次点击按钮,内存升至190MB且从未下降,即使手动调用垃圾回收器也无效果。请问为何方法执行完毕后应用仍持续占用内存?
.NET GC的工作逻辑
.NET的垃圾回收器不会在对象失去引用后立刻释放内存,它会根据内存压力自动触发回收。哪怕你手动调用GC.Collect(),也只是给GC一个“可以回收了”的建议,不是强制立刻清空所有可回收内存。尤其你创建的10万个元素的列表属于大对象堆(LOH),LOH的回收频率比普通小对象堆低很多,而且默认不会压缩内存——回收后的空间会被标记为可用,但不会立刻还给操作系统,所以进程的内存占用数值看起来没降。局部变量的生命周期可能被延长
你在Search方法里的lstSearch变量,虽然方法执行完了,但JIT编译器可能为了调试方便延长它的生命周期,导致对象暂时无法被GC回收。另外,返回的PartialView如果内部隐式引用了这个列表(哪怕你觉得没用到),也会让对象一直被持有,没法回收。字符串驻留的影响
循环里反复用的"asd"字符串会被CLR的字符串驻留池缓存,这些字符串是全局引用的,不会被GC回收,不过这部分占的内存很小,不是主要问题。进程内存的“保留”策略
.NET进程会保留曾经分配过的内存空间,避免频繁向操作系统申请/释放内存带来的性能损耗。就算GC回收了内存,这部分空间也会被进程留着,等后续需要分配内存时直接用,所以你看到的进程内存占用不会马上下降,只有当内存压力足够大或者进程空闲时,才会逐步把内存还给操作系统。
- 用Visual Studio的内存诊断工具(Memory Profiler)抓内存快照,看看哪些对象还在被引用,确认有没有隐式的引用泄漏。
- 别手动调用
GC.Collect(),这会打乱GC的自动优化策略,反而影响性能。 - 如果不需要返回列表数据,在方法结束前主动把
lstSearch设为null,帮GC更快识别可回收对象。 - 针对大对象堆的问题,可以考虑拆分数据,别一次性创建这么大的列表;如果用的是.NET Core/.NET 5+,可以通过配置
System.GC.LOHCompactionMode启用LOH压缩,提升内存利用率。
内容的提问来源于stack exchange,提问作者Diego

