什么对象会被 .NET 垃圾回收
2022 年 9 月 13 日 · 2 分钟读完
原来的笔记只记了两种情况,都对:
1. 对象引用超出作用范围

2. 把引用赋值为 null

这两种情况的本质是一样的:没有任何「根」还能找到这个对象了。下面把背后的规则补完整。
GC 怎么判断谁是垃圾
GC 不数引用次数,而是从一组「根」出发,顺着引用把能走到的对象都标记为活的,剩下走不到的就是垃圾。根包括:
- 正在执行的方法里的局部变量和参数(准确地说是 JIT 认为之后还会用到的那些);
- 静态字段;
- CPU 寄存器里的引用;
- 被
GCHandle钉住的对象、终结队列里的对象。
所以两个对象互相引用、但外面没人引用它们时,照样会被回收,不会像引用计数那样漏掉循环引用。
一个容易误会的细节:Release 模式下,局部变量在最后一次使用之后就可能被回收,不用等到方法结束。调试模式(Debug)为了方便看变量,会把生命周期延长到方法结束,所以两种模式下行为可能不同。
什么时候回收
成了垃圾不等于马上被回收。GC 在这些时候运行:
- 第 0 代的分配额度用完(最常见);
- 系统内存不足;
- 手动调用
GC.Collect()(一般不要这么做)。
.NET 把堆分成三代:新对象在第 0 代,熬过一次回收升到第 1 代,再熬过升到第 2 代。大多数对象朝生暮死,所以第 0 代回收得又快又频繁;大于 85000 字节的对象直接放进大对象堆(LOH),和第 2 代一起回收。
延伸:上位机里最常见的「内存泄漏」
C# 有 GC 还会内存泄漏,几乎都是因为对象还被某个根引用着,只是你没意识到:
- 事件没取消订阅:窗体 A 订阅了一个全局对象(比如 PLC 通讯类)的事件,
publisher.DataReceived += OnData;,A 关闭后,全局对象仍然通过事件持有 A 的引用,A 和它的所有控件都回收不掉。关窗体时记得-=。见 C# 委托与事件。 - 静态集合越加越多:静态的
List或Dictionary当缓存用,只加不删。 - Timer 没停:
System.Timers.Timer和System.Threading.Timer运行时会被运行时持有,不Dispose就一直活着,回调里引用的对象也活着。 - 非托管资源没释放:GC 只管托管内存。Halcon 的
HObject、相机 SDK 的图像缓冲、Bitmap、文件句柄都占着非托管内存或句柄,必须Dispose()或用using。连续采图的程序里漏掉一个Dispose,几个小时就能把内存吃满。
排查时可以用 VS 的「诊断工具」拍两次内存快照对比,或者用 dotnet-counters、dotnet-gcdump 看哪种对象一直在涨。
原文首发于 blog.csdn.net