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;
<pre class='brush:php;toolbar:false;'>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内部输出。
