GC.WaitForPendingFinalizers 之后未调用终结器

Finalizer not called after GC.WaitForPendingFinalizers

我阅读了 this fantastic explanation from Eric Lippert 关于何时通过事件引用另一个对象的对象被垃圾收集的内容。

为了证明 Eric 所说的,我尝试了这段代码:

using System;
                
public class Program
{
    public static void Main()
    {
        {
            var myClass = new GCClass();
            LongLivingClass.MyEvent += myClass.HandleEvent;
        } // LongLivingClass holds a reference to myClass
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        // expect: finalizer of GCLass not run

        {
            var myClass = new GCClass();
            myClass.MyEvent += LongLivingClass.HandleEvent;
        } // myClass can easily be GCed here
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        // expect: finalizer should run

        Console.WriteLine("Finish");
    }
}


class GCClass
{
    public event EventHandler MyEvent;
    public void HandleEvent(object o, EventArgs e) { }
    ~GCClass() { Console.WriteLine("Finalizer hit"); }
}    
public class LongLivingClass
{
    public static event EventHandler<EventArgs> MyEvent;
    public static void HandleEvent(object o, EventArgs e) { }
}

正如我所料,第一个 GC.Collect 块没有完成任何事情,因为对象只是被 LongLvongClass 引用,因此在集合中幸存下来。

然而,第二个块也没有调用终结器,尽管 myClass 符合收集条件,我们甚至在等待终结器发生。但是,我的终结器没有被击中。从 GC.Collect() and Finalize 开始,我希望终结器会在此处命中。我在其中放置了一个断点,以表明这一点。

我哪里错了?我想 myClass 没有收集到第二个代码块中,但我不知道为什么。

Where did I go wrong here?

简短版本:

这里要认识到的重要一点是 C# 不像 C++ ,其中 } 的意思是“我们现在必须 运行 局部变量的析构函数”。

C# 可以自行决定增加 any 局部变量的生命周期,而且它一直这样做。因此,您永远不应期望仅仅因为控制进入变量超出范围的区域就收集局部变量的内容。

长版:

{ }定义嵌套局部变量声明的规则space是C#语言的规则;强调这不是 C# 编译成的 IL 的一个特性!

那些局部变量声明 space 在那里,以便您可以更好地组织您的代码,以便 C# 编译器可以找到您使用不在范围内的局部变量的错误。但是 C# 不会在 { } 块的底部发出 IL,表示“以下局部变量现在超出范围”。

允许 注意到您的第二个 myClass 从未被读取,因此可能会在最终写入后从根集中删除。但是,允许这样做的事实并不要求这样做,通常情况下也不会。

“通常”在那里做一些繁重的工作,因为当然允许抖动 缩短 本地的生命周期。考虑这种糟糕的情况:

{
    ManagedHandle handle = NativeObject.Open();
    SomeNativeCode.CacheTheUnderlyingHandle(handle);
    // handle is still in scope but the jitter is allowed to
    // realize it is never read again at this point and remove the
    // local from the root set. If you have the misfortune that you
    // get a GC and finalize right now then the resource is deallocated
    // by the time we get to:
    SomeNativeCode.UseTheCachedHandle();
}        

如果您遇到这种不幸的情况,请使用 KeepAlive 迫使当地人活下来。