Finalizer中只能安全释放本机资源,禁止调用托管对象方法、访问非静态成员、抛出异常或使用同步机制;推荐用IDisposable+SafeHandle替代。

Finalizer 中只能做极有限的资源清理,不能调用托管对象方法
Finalizer(即 ~ClassName())不是普通方法,它由 GC 在单独的终结线程上调用,此时对象已处于“半销毁”状态:引用的其他托管对象可能已被回收或正在被终结。因此,Finalizer 中禁止:
- 调用任何托管对象的实例方法(包括
ToString()、Dispose()、Close()等) - 访问任何非
static字段或属性(哪怕它们是int或bool)——因为字段所属对象可能已不可达 - 抛出异常(会终止终结线程,后续对象可能永远不被清理)
- 等待同步原语(如
Monitor.Enter、Task.Wait()、Thread.Sleep()),会导致终结队列阻塞
唯一安全的操作是:释放**本机资源**(如 IntPtr 指向的内存、文件句柄、GDI 句柄),且必须使用 Marshal.FreeHGlobal()、CloseHandle() 等底层 Win32 API 或等效跨平台机制。
Finalizer 绝对不是线程安全的执行环境
GC 的终结线程是全局唯一的(.NET 5+ 默认单线程终结器),但它不保证与你的代码线程隔离。更关键的是:
- 多个对象的
Finalizer可能并发执行(尤其在 .NET Framework 多终结器线程模式下) - 你无法控制
Finalizer执行时机,也无法知道它和你主线程/工作线程的相对执行顺序 -
lock、Interlocked、ConcurrentDictionary等线程同步机制在Finalizer中不可靠甚至危险——因为依赖的托管类型(如object实例)本身可能正被终结
所以,不要试图在 Finalizer 内做任何需要线程协调的操作。若必须清理共享本机资源,请用 static 全局锁 + 原子操作(如 Interlocked.CompareExchange 配合 IntPtr.Zero 标记),但前提是该资源生命周期完全独立于托管堆。
为什么你几乎不该写 Finalizer?IDisposable + SafeHandle 是现代替代方案
.NET 推荐用 IDisposable 显式释放,而 Finalizer 仅作为“最后兜底”。但手动写 Finalizer 极易出错,正确模式应是:
- 继承
SafeHandle(如SafeFileHandle、自定义SafeHandle子类)封装本机句柄 -
SafeHandle自带受保护的ReleaseHandle(),由 GC 安全调用,无需手写Finalizer - 你的类只实现
IDisposable,并在Dispose(bool)中调用_handle.Dispose()
public class MyResource : IDisposable
{
private readonly SafeFileHandle _handle;
public MyResource(string path) => _handle = File.OpenHandle(path, ...);
public void Dispose() => Dispose(true);
protected virtual void Dispose(bool disposing)
{
if (disposing) { /* 托管资源 */ }
_handle?.Dispose(); // SafeHandle 保证线程安全且可重入
}
// ❌ 不要写 ~MyResource() —— SafeHandle 已接管终结逻辑}
Finalizer 触发时,连 Console.WriteLine 都可能失败
常见误操作是想在 Finalizer 中打日志验证行为,例如:
~MyClass()
{
Console.WriteLine("Finalizer running"); // ❌ 危险!Console 可能已被卸载
}此时 Console 是托管对象,其内部缓冲区、同步锁、输出流都可能已失效。同理,Debug.WriteLine、EventLog.WriteEntry、File.WriteAllText 全部不可用。唯一勉强可用的底层输出是 NativeMethods.OutputDebugString(Windows)或直接写入 /dev/null 文件描述符(Linux/macOS),但依然不推荐。
真正需要诊断终结行为时,应改用 WeakReference + 主动轮询,或启用 DOTNET_GCLOG 等运行时日志,而非依赖 Finalizer 内部输出。










