短弱引用何时变为空?

8
我在我的类Foo中使用WeakReference<T>短弱引用)来跟踪一个对象。这个类有一个析构函数,在其中我需要访问被跟踪的对象。我跟踪的对象还使用WeakReference<Foo>跟踪Foo
现在我想知道,“零化”WeakReference发生在什么时候?所有WeakReference是否在运行任何终结器之前都会被置空,或者它们每个都会在要运行其跟踪对象的终结器之前被置空? 更新 现在我也想知道Mono项目是否可以解决这个问题(链接1, 链接2)。但我有点担心MS GCMono GC可能会以不同的方式处理这个问题,导致它们不兼容。

哇,文档写得太混乱了......看起来简短的弱引用文档应该是在讨论终结而不是垃圾回收 - 但是不够清晰。 - Jon Skeet
我浏览了旧的SSCLI源代码,试图找到一些有用的东西。目前还没有找到明确的东西,但在Target getter内发现了一个有趣的注释:“只有在非法使用时才会发生,例如从终结器中使用WeakReference” - 所以也许你正在尝试做的事情注定会失败。 - Damien_The_Unbeliever
1
不是答案,但值得注意的是,通常应避免使用析构函数,这很可能是一个 XY 问题。 - H H
@HenkHolterman 我正在实现一个弱字典,应该自动清理空的弱引用。我几乎无法想象在没有析构函数的情况下如何实现这一点。 - Paya
需要清理的东西不应该放在弱引用字典中。 - H H
我说的是空引用。就像WeakReference指向真实对象,但由于这些对象已被回收,所以WeakReference.Target现在为空。然而,WeakReference对象仍然存在于字典中,浪费空间。 - Paya
3个回答

5

我计划写一个小的演示程序来展示差异。结果比我想象的要具有挑战性。第一个必要条件是确保终结器线程可以减慢速度,这样您就可以观察到WeakReference.IsAlive的值,而不必担心它受到与终结器线程竞争的影响。因此我使用了:

class FinalizerDelayer {
    ~FinalizerDelayer() {
        Console.WriteLine("Delaying finalizer...");
        System.Threading.Thread.Sleep(500);
        Console.WriteLine("Delay done");
    }
}

接下来是一个将作为WeakReference目标的小类:

class Example {
    private int instance;
    public Example(int instance) { this.instance = instance; }
    ~Example() {
        Console.WriteLine("Example {0} finalized", instance);
    }
}

然后有一个程序,演示了长弱引用和短弱引用之间的区别:
class Program {
    static void Main(string[] args) {
        var target1 = new Example(1);
        var target2 = new Example(2);
        var shortweak = new WeakReference(target1);
        var longweak = new WeakReference(target2, true);
        var delay = new FinalizerDelayer();
        GC.Collect();       // Kills short reference
        Console.WriteLine("Short alive = {0}", shortweak.IsAlive);
        Console.WriteLine("Long  alive = {0}", longweak.IsAlive);
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Finalization done");
        GC.Collect();       // Kills long reference
        Console.WriteLine("Long  alive = {0}", longweak.IsAlive);
        Console.ReadLine();
    }
}

您必须运行此程序,以便调试器不会影响对象的生存期。选择发布构建并更改调试器设置:工具 + 选项,调试,常规,取消选中“抑制JIT优化”选项。
结果证明对象的终结顺序确实是不确定的。每次运行程序时,顺序都不同。我们希望FinalizerDelayer对象首先得到终结,但这并不总是发生。我认为这是内置的地址空间布局随机化特性的副作用,它使托管代码非常难以攻击。但是,如果经常运行它,您最终会获得以下结果:
延迟终结……
      短时间存活=假
      长时间存活=真
      延迟完成
      示例1已完成终结
      示例2已完成终结
      终结完成
      长时间存活=假
长话短说:
- 短弱引用在对象被收集并放置在可回收队列上时立即将IsAlive设置为false,准备进行终结。该对象仍然存在,但没有强引用存在,它很快就会被终结。 - 长弱引用跟踪对象的整个实际生命周期,包括其在可回收队列上的生命周期。直到其终结程序完成,IsAlive才会被设置为false。
当对象被复活并从可回收队列移回正常堆时(重新创建强引用时),请注意一个怪癖。这不是我在此演示程序中探讨的内容,但需要长弱引用才能观察到。您需要长弱引用的基本原因。

4
您可以通过一个简单的测试程序来验证。但是,我发现与您查看的页面相比,WeakReference类型本身的文档更加清晰明了。
特别是,在您链接的页面中所称为“short”和“long”的标志在实际构造函数文档中被称为trackResurrection。参数的描述如下:

指示何时停止跟踪对象。如果为 true,则在终结后跟踪对象;如果为 false,则仅在终结前跟踪对象。

“备注”部分还写道:

如果 trackResurrection 为 false,则创建短弱引用。如果 trackResurrection 为 true,则创建长弱引用。

这证实了当您使用“短”弱引用时,已终结的对象将不再被WeakReference对象跟踪(即Target变为null),但当您使用“长”弱引用时,它将被跟踪。
对于两种类型的弱引用,实际上已经进行垃圾回收的对象肯定不会再被跟踪(显然)。
通常情况下,在最终器线程实际执行其工作时,程序中的其他线程都不应该能够观察到对象,因此对于“短”弱引用在Target属性设置为null的确切时刻似乎无关紧要。如果程序中的某个其他线程观察到该值为非空,则最终器尚未运行。如果它观察到它为空,则最终器已经运行。那个“其他线程”不应该在最终器线程工作时自己运行,因此对于那个“其他线程”而言,终结基本上是原子的。

0
经过长时间的研究,我找到了这篇古老的文章.NET框架中的垃圾回收第二部分:自动内存管理第一部分讨论了对象复活和可回收队列):
现在,当垃圾回收(GC)运行时会发生以下情况:
  1. 垃圾收集器构建所有可达对象的图形。本文第一部分讨论了如何完成此操作。

  2. 垃圾收集器扫描短弱引用表。如果表中指针所引用的对象不属于该图形,则该指针标识一个不可达对象,而短弱引用表中的插槽被设置为 null

  3. 垃圾收集器扫描终结队列。如果队列中的指针引用的对象不属于图形,则该指针标识一个不可达对象,并将该指针从终结队列移动到 freachable 队列。此时,由于对象现在被认为是可达的,因此将对象添加到图形中。

  4. 垃圾收集器扫描长弱引用表。如果表中指针所引用的对象不属于该图形(该图形现在包含 freachable 队列中条目所指向的对象),则该指针标识一个不可达对象,插槽被设置为 null。

  5. 垃圾收集器压缩内存,挤出不可达对象留下的空洞。

所以,即使我的类Foo有一个finalizer,因此它将在freachable队列中(被认为是根) - 短弱引用的解析发生在此对象在freachable队列中成为根之前,这意味着短弱引用将为空:

垃圾收集器一旦确定对象不可达就会在短弱引用表中将指针设置为null。如果对象具有Finalize方法,则该方法尚未被调用,因此对象仍然存在。如果应用程序访问WeakReference对象的Target属性,则即使对象实际上仍然存在,也将返回null。


此外,正如Yun Jin的WebLog所提到的那样,在finalizer中引用finalizable对象通常不是一个好习惯,但是WeakReference是一个例外(尽管这并不总是这样)。由于WeakReference是一个带有finalizer的对象,如果它在Foo的finalizer中被访问,它可能已经被finalize了(在这种情况下,Target属性将始终返回null,尽管跟踪的对象可能仍然存活良好(更多信息)。

我刚刚确认Mono垃圾回收器与此行为一致。


有用的参考资料:
- WeakReference 源代码
- WeakReference<T> 源代码


"由于WeakReference是一个带有finalizer的对象" - 你确定你还在正确的轨道上吗? - H H
@HenkHolterman 这确实会让事情变得非常复杂,但希望我能想办法绕过这个问题。比如通过继承自WeakReference并在其中放置所有我所需的复杂逻辑。:-)GC处理的顺序绝对关键,必须正确理解。 - Paya

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接