← 全部文章 C# 语言

什么对象会被 .NET 垃圾回收

2022 年 9 月 13 日 · 2 分钟读完

目录
  1. GC 怎么判断谁是垃圾
  2. 什么时候回收
  3. 延伸:上位机里最常见的「内存泄漏」

原来的笔记只记了两种情况,都对:

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 看哪种对象一直在涨。

# C# # GC # CLR

原文首发于 blog.csdn.net