根本原因是对象仍被强引用持有,gc只回收不可达对象;常见于loh上大数组长期驻留、静态缓存未清理、事件订阅未取消、字符串驻留及闭包隐式捕获等场景。

为什么.NET应用内存持续上涨,但GC不回收
根本原因不是GC失灵,而是对象仍被强引用持有——GC只回收“不可达”对象。常见现象是:任务管理器显示内存涨到2GB,!dumpheap -stat 却只看到几百MB托管对象,差值基本落在非托管内存、JIT代码、线程栈或LOH未触发回收上。
关键判断点:!eeheap -gc 输出的“GC Heap Size”才是真实托管堆压力;若该值稳定,但工作集(Working Set)飙升,大概率是LOH膨胀或非托管泄漏。
- LOH上的对象(≥85000字节)只在Gen2 GC时才回收,且不压缩——容易碎片化、长期驻留
- 大数组(如
byte[]、string)高频分配又没及时释放,会快速填满LOH - 异步流处理、图片/视频编码、序列化大Payload等场景,极易无意中触发LOH分配
如何用WinDbg快速定位LOH泄漏源头
别一上来就跑!dumpheap -stat 全量扫描——慢且干扰多。直接锁定LOH,再顺藤摸瓜。
- 先筛大对象:
!dumpheap -min 85000 -stat,重点关注System.Byte[]、System.String、自定义大类(如MyReportGenerator)的Total Size - 挑一个典型实例地址(如
000001a2f3456789),执行:!gcroot -all 000001a2f3456789 - 重点看
HandleTable和FinalizerQueue:前者说明有静态事件、Timer、缓存没清理;后者提示终结器线程卡住或对象没被正确Dispose - 若
!gcroot输出里反复出现某个静态类(如CacheManager),立刻检查其内部集合是否无界增长
服务器GC与容器部署下内存翻倍的真实原因
Linux容器中.NET默认启用服务器GC,但它会读取cgroup内存限制来预估堆大小。若容器没设内存limit,运行时可能按宿主机总内存估算,导致预留堆空间远超实际需要——表现为内存占用翻倍,但!eeheap -gc 显示堆实际使用率很低。
- 验证当前模式:
Console.WriteLine($"IsServerGC: {GCSettings.IsServerGC}"); - 强制硬限制(推荐):
COMPlus_GCHeapHardLimit=536870912(512MB),避免堆无限预留 - 禁用自动堆伸缩(可选):
DOTNET_GCHighMemPercent=70,防止低负载时也维持大堆 - 注意:Windows服务默认工作站GC,改服务器GC需显式配置
DOTNET_gcServer=1,否则IsServerGC仍为false
字符串驻留和匿名闭包——看不见的强引用陷阱
这两类泄漏不会出现在!dumpheap -stat 的“大对象”列表里,但能撑爆内存:驻留字符串永久存活在全局池中;闭包捕获的this或局部变量,会让整个对象图无法释放。
-
string.Intern()是单向操作——一旦驻留,除非AppDomain卸载,否则永不释放。高并发日志、动态SQL拼接等场景慎用 - Lambda订阅事件时,如果写成
button.Click += (s,e) => this.DoWork();,就隐式捕获了this;改用静态方法或显式取消订阅 - 检查闭包泄漏:用
!dumpheap -type YourNamespace+c__DisplayClass查看编译器生成的闭包类实例数是否持续增长
真正难处理的从来不是“找不到泄漏”,而是“找到后不敢动”——比如静态缓存被几十个模块隐式依赖,或第三方SDK内部持有了你无法控制的根引用。这时候,!gcroot 输出里的每一行,都是必须逆向追踪的调用链。











