【问题标题】:When do short weak references become null?短弱引用何时变为空?
【发布时间】:2015-05-22 06:16:03
【问题描述】:

我在班级Foo 中使用WeakReference<T> (short weak reference) 跟踪对象。这个类有一个析构函数,我需要在其中访问那个被跟踪的对象。我跟踪的对象也在使用WeakReference<Foo> 跟踪Foo

所以现在我想知道,WeakReference 的“归零”究竟是什么时候发生的?是否所有WeakReference 在运行任何终结器之前都被清空,或者它们每个都在它们跟踪的对象的终结器即将运行之前被清空?

更新

现在我也想知道Mono 项目是否可以阐明这一点(link 1link 2)。但我有点担心MS GCMono GC 可能会以不同的方式处理这个问题并且不兼容

【问题讨论】:

  • 哇,这令人困惑的文档......它看起来像简短的弱参考文档应该谈论终结,而不是垃圾收集 - 但不清楚。
  • 我浏览了旧的 SSCLI 源代码,想看看我能找到什么。还没有找到任何确定的东西,但确实在Target getter 中找到了一个有趣的评论:“应该只在非法使用时发生,比如使用终结器中的 WeakReference”——所以也许你想做的事情无论如何都注定要失败。
  • 不是答案,但值得注意的是,您通常应该避免使用析构函数,这很可能是 XY 问题。
  • @HenkHolterman 我正在实现一个弱字典,它应该自动清理无效的弱引用。我很难想象没有析构函数你怎么能做到这一点。
  • 需要清理的东西不应该放在弱字典中。

标签: c# memory-management garbage-collection weak-references


【解决方案1】:

我想写一个小演示程序来演示差异。结果比我想象的更具挑战性。第一个必要的因素是确保终结器线程可以减慢,这样您就可以观察 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();
    }
}

您必须运行此程序,以便调试器不会影响对象的生命周期。选择 Release build 并更改调试器设置:Tools + Options、Debugging、General,取消勾选“Suppress JIT optimization”选项。

原来对象的最终确定顺序确实是不确定的。每次运行程序时顺序都不同。我们希望 FinalizerDelayer 对象首先完成,但这并不总是发生。我认为这是内置地址空间布局随机化功能的副作用,它使托管代码很难被攻击。但是经常运行它,你最终会得到:

延迟终结器...
短命=假
长寿 = 真
延迟完成
示例 1 完成
示例 2 完成
完成完成
长寿 = 假

长话短说:

  • 只要对象被收集并放置在易碎队列中,就可以将 IsAlive 设置为 false,以等待最终确定。该对象在物理上仍然存在,但不再存在强引用,它将很快完成。
  • 长弱引用在对象的真实生命周期内一直跟踪对象,包括它在易碎队列中的生命周期。 IsAlive 在其终结器完成之前不会设置为 false

当对象被复活时要当心一个怪癖,当一个强引用被重新创建时,它会从 freachable 队列移回普通堆。不是我在这个演示程序中探索的东西,而是需要一个很长的弱参考来观察它。您需要长弱引用的基本原因。

【讨论】:

    【解决方案2】:

    您可以通过一个简单的测试程序自行验证。但我发现 WeakReference 类型本身的文档比您正在查看的页面更清晰。

    特别是,链接页面中称为“短”和“长”的标志在the actual constructor documentation 中称为trackResurrection。参数说明如下:

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

    “备注”部分还写着:

    如果 trackResurrection 为 false,则创建一个短弱引用。如果 trackResurrection 为真,则创建一个长弱引用。

    这证实了当您使用“短”弱引用时,已完成的对象将不再被WeakReference 对象跟踪(即Target 变为null),但是当您使用“长”对象时弱引用,会的。

    对于这两种弱引用,实际上已经被垃圾回收的对象肯定不再被跟踪(显然)。

    一般来说,当终结器线程实际工作时,程序中没有其他线程应该能够观察对象,因此当Target 属性设置为@ 时,“短”弱引用的精确时刻987654328@ 对我来说似乎没有意义。如果程序中的某个其他线程观察到该值为非空值,则终结器尚未运行。如果它观察到它为空,则终结器已运行。 “其他线程”本身不应该在终结器线程工作时运行,因此就“其他线程”而言,终结本质上应该是原子的。

    【讨论】:

      【解决方案3】:

      所以经过大量研究,我找到了这篇古老的文章Garbage Collection Part 2: Automatic Memory Management in the Microsoft .NET Frameworkpart 1 讨论了复活和易碎队列):

      现在,垃圾收集 (GC) 运行时会发生以下情况:

      1. 垃圾收集器构建所有可达对象的图表。本文的第 1 部分讨论了它的工作原理。

      2. 垃圾收集器扫描短弱引用表。如果表中的指针引用了不属于图的对象,则该指针标识了一个不可访问的对象,并且短弱引用表中的槽被设置为空

      3. 垃圾收集器扫描终结队列。如果队列中的指针指向不属于图的对象,则指针标识不可达对象,并且指针从终结队列移动到可触及队列。此时,该对象被添加到图表中,因为该对象现在被认为是可访问的。

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

      5. 垃圾收集器压缩内存,挤出无法访问的对象留下的空洞。

      所以即使我的类Foo 有一个终结器,因此它会在一个可访问队列中(被认为是一个根) - 短弱引用的解析发生在此对象植根于可访问队列之前,这意味着短弱引用将为空:

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


      此外,正如Yun Jin's WebLog 中提到的,在终结器中引用可终结对象通常不好,但WeakReference 有点例外(尽管this was not always the case)。由于WeakReference 是一个带有终结器的对象,如果在Foo 的终结器中访问它,它可能已经终结(在这种情况下,Target 属性将始终返回 null,尽管跟踪的对象可能仍然是活得好好的 (more info)。


      我刚刚确认Mono garbage collector与此行为一致。


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

      【讨论】:

      • “因为 WeakReference 是一个带有终结器的对象” - 你还确定你在正确的轨道上吗?
      • @HenkHolterman 它确实使事情复杂化了很多,但希望我能以某种方式解决这个问题。例如通过从 WeakReference 继承并将我需要的所有花哨的逻辑放在那里。 :-) GC 执行的顺序对于正确处理绝对至关重要。
      猜你喜欢
      • 2013-07-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-27
      • 1970-01-01
      相关资源
      最近更新 更多