终结器不是自动清理工具,而是最后一道防线;它不该被主动设计进日常资源管理流程,而只应在idisposable被遗漏时兜底。滥用会导致对象多活至少一个gc周期、finalizer线程阻塞、甚至进程崩溃。

终结器不是“自动清理工具”,而是最后一道防线;它不该被主动设计进日常资源管理流程,而只应在 IDisposable 被遗漏时兜底。滥用会导致对象多活至少一个 GC 周期、Finalizer 线程阻塞、甚至进程崩溃。
~ClassName() 语法到底做了什么
写 ~Car() 并不是定义一个普通方法,而是让 C# 编译器生成一个隐式重写的 protected override void Finalize()。这个方法会被 GC 在特定时机调用,并且编译器会自动在末尾插入 base.Finalize()(除非你禁用了该行为,比如用了某些旧版编译器或非 C# 语言)。
这意味着:
- 它不是构造函数的反向操作,不保证与构造顺序对称
- 它无法访问
virtual成员(因为对象可能已部分析构) - 它运行在线程池中的专用 Finalizer 线程 上,而非你调用
new的那个线程 - 它不会被继承链自动传播——但 C# 编译器帮你补上了
MyBase.Finalize()
Finalizer Queue 不是队列,是带标记的链表
GC 发现一个不可达对象带有终结器时,不会立刻执行清理,而是把它挂到内部的 Finalizer Queue(实际是链表+标记位)。这个动作本身不耗时,但会让对象“卡住”至少一次 GC 周期。
常见误解和后果:
- 以为“加了 ~MyClass() 就等于安全了” → 实际上只是延迟释放,且不可控
- 在
Finalize()里做同步 I/O 或锁等待 → 阻塞整个 Finalizer 线程,导致后续所有终结器积压 - 高频 new 带终结器的对象(如每帧创建)→
Finalizer Queue积压,GC 压力陡增,内存占用虚高 - 终结器中抛出未捕获异常 → .NET 5+ 默认直接终止进程(不是吞掉)
GC.SuppressFinalize(this) 必须在 Dispose(true) 末尾调用
这是终结器和 IDisposable 协同工作的关键开关。它的作用不是“取消终结”,而是告诉 GC:“这个对象我已经手动清理过了,别再把它扔进 Finalizer Queue。”
正确位置只有一处:
- 在
Dispose(bool disposing)方法中,当disposing == true时,在释放完托管/非托管资源之后、方法返回之前立即调用GC.SuppressFinalize(this)
错误做法包括:
- 只在 public
Dispose()外层调用,没进Dispose(true)分支 → 终结器仍会执行 - 在
Finalize()里调用 → 已无意义,对象正被终结 - 在构造失败后调用 →
this可能处于无效状态
为什么 CA2216 要求“可释放类型应声明终结器”
代码分析规则 CA2216 的出发点是防御性编程:如果你的类持有非托管资源(比如 IntPtr、SafeHandle 子类以外的句柄),又实现了 IDisposable,那它就应该有终结器作为最后保险。
但它不等于鼓励你写终结器。真正推荐的做法是:
- 优先用
SafeHandle包装非托管资源(它自带可靠终结逻辑) - 若必须手写终结器,确保只清理非托管部分,且绝不调用任何托管对象成员
- 永远不要写空终结器(
~MyClass() { })——它白白往Finalizer Queue插入条目
最常被忽略的一点:终结器里不能依赖任何托管状态,包括字段值可能已被回收或重置;它唯一能安全做的,就是调用 Win32 API 关闭句柄、调用 Marshal.FreeHGlobal 等确定性释放操作。











