【问题标题】:Testing/Verifying a WeakReference测试/验证弱引用
【发布时间】:2011-05-06 14:24:04
【问题描述】:

我想验证设置WeakReference 的代码不会意外持有对被引用对象的强引用。 (这里是 an example 说明如何很容易意外地做到这一点。)

这看起来是检查无意强引用的最佳方法吗?

TestObject testObj = new TestObject();
WeakReference wr = new WeakReference(testObj);

// Verify that the WeakReference actually points to the intended object instance.
Assert.Equals(wr.Target, testObject);

// Force disposal of testObj;
testObj = null;
GC.Collect();
// If no strong references are left to the wr.Target, wr.IsAlive will return false.
Assert.False(wr.IsAlive);

【问题讨论】:

  • 你不能指望 GC.Collect() 来强制 gc 收集垃圾,这只是一个建议,所以它可能不会删除对象。 Automatic Memory Collection in .Net
  • 您介意详细说明一下为什么 GC.Collect() 可能不会破坏符合收集条件的对象吗?
  • 显然它强制进入默认模式。只有当它设置为优化模式时才没有,我没有意识到。
  • 嗯,这只是 Java 中的一个建议(gc() 方法)。也许我只是认为它会在 C# 中做同样的事情。
  • @BenGribaudo 我看到一个奇怪的行为,如果不是 Assert.False(wr.IsAlive);我使用 if(wr.IsAlive) Console.WriteLine("Memory Leak");我确实看到正在打印“内存泄漏”并验证 testObj 实例在这种情况下不会被垃圾收集,因此会出现错误。我什至添加了 GC.WaitForPendingFinalizers(); GC.Collect();在 GC.Collect() 之后;多于。这仅在检查是否在 Debug.Assert 或 Assert 内时才有效。

标签: c# unit-testing weak-references


【解决方案1】:

我就此与 Microsoft 取得了联系,并了解到/确认:

  • GC.Collect() 强制进行阻塞式垃圾回收。
  • GC.Collect() 运行时,它不会神秘地跳过符合收集条件的对象。遵循可预测的规则来确定要收集哪些对象。只要您了解这些规则(即如何处理可终结对象),就可以强制销毁特定对象,尽管被销毁对象使用的内存可能会或可能不会被释放。

更多信息见我的博客:Can .Net garbage collection be forced?

【讨论】:

    【解决方案2】:

    涉及WeakReference 对象的单元测试比您想象的要复杂。正如您和其他人所指出的,GC.Collect() 可能会“强制”进行垃圾回收,但这仍然取决于您的对象没有对其的引用。

    不幸的是,如何构建代码可能会改变对象是否仍然具有对它们的引用。更具体地说,无论您是在 Debug 还是 Release 模式下构建,都可以并且将在对象仍然植根时发生变化(更准确地说,取决于您是否打开了优化;Debug 默认关闭它们,而 Release 默认打开它们) .调试模式关闭了很多优化,它甚至倾向于根目录在当前正在执行的方法中创建/声明的对象。因此,您的单元测试可能在 Debug 构建中失败,但在 Release 构建中成功。

    在您的示例中,即使您将 testObj 设置为 NULL,编译器仍试图通过保持其先前值的根来在 Debug 构建中提供帮助。这意味着无论您调用多少次GC.Collect()wr.IsAlive 将始终返回 TRUE。

    那么,你怎么能测试WeakReferences?很简单:在另一种方法中创建它们以及它们所基于的对象。只要该方法没有内联,而且在大多数情况下不会内联,编译器就不会根您关心的对象,并且您可以让您的测试在调试和发布版本中都通过。

    下面的函数为您提供了如何执行此操作的提示:

    public static Tuple<WeakReference, ManualResetEvent, int> GetKillableWr(Func<object> func, bool useGetHashCode = false)
    {
        var foo = func();
        var result = new Tuple<WeakReference, ManualResetEvent, int>(new WeakReference(foo), new ManualResetEvent(false), useGetHashCode ? (foo?.GetHashCode() ?? 0) : RuntimeHelpers.GetHashCode(foo));
    
        Task.Factory.StartNew(() =>
        {
            result.Item2.WaitOne();
            GC.KeepAlive(foo);  // need this here to make sure it doesn't get GC-ed ahead of time
            foo = null;
        });
    
        return result;
    }
    

    使用这个,只要你在func参数内创建你的对象,你就可以为你选择的对象创建一个WeakReference在您发出返回的ManualResetEvent 并调用GC.Collect() 后,就不会被root。正如其他人所指出的,调用以下代码以确保在需要时进行清理会很有帮助...

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
    

    编辑:

    还有一些其他的“陷阱”需要担心。一个常见的涉及Strings。 String 文字和常量总是有根的,因为它们被编译为您的 DLL/EXE 的引用。因此,new WeakReference("foo") 之类的东西将始终显示为活动的,因为“foo”已存储到您的 DLL 中,并且在编译代码中提供了对该存储文字的引用。解决此问题的一种简单方法是使用 new StringBuilder("&lt;your string here&gt;").ToString() 而不是字符串文字。

    再次编辑:

    另一个“陷阱”是在 Release 构建中,优化会导致 GC 更加激进,与上述情况不同,这可能会导致对象比您预期的更快超出范围。在下面的代码中,wr.IsAlive 有时会返回 FALSE,因为 GC 检测到 myObject 不会被方法中的其他任何东西使用,因此它可以进行垃圾回收。解决此问题的方法是将GC.KeepAlive(myObject) 放在方法的末尾。这将使myObject 保持扎根,直到至少执行该行。

    public static void SomeTest()
    {
        var myObject = new object();
        var wr = new WeakReference(myObject);
        GC.Collect();
        Assert.True(wr.IsAlive, "This could fail in Release Mode!");
    }
    

    【讨论】:

    • 这实际上是我见过的众多解决方案中唯一可行的解​​决方案。
    • 这适用于 net461,但不适用于 net5。
    【解决方案3】:

    我昨天才这样做。这是我必须添加的内容,以确保集合发生在您上次断言之前:

            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.WaitForFullGCComplete();
            GC.Collect();
    

    如果在此之后 .IsAlive 仍然为真,则很可能某处仍有强引用。

    顺便说一句 - 当您访问 WeakReference 目标时,请务必不要检查 .IsAlive。要避免检查 .IsAlive 和 .Target 之间的竞争条件,请执行以下操作:

    var r = weakRef.Target AS Something;
    if (r != null)
    {
        ... do your thing
    }
    

    【讨论】:

    • 代码没有确保对象被收集,你不能强制GC。如果您进行双重检查,您仍然可以使用IsAlive,即在获得目标后还检查是否为空。如果您希望尽早跳过检查,这可能很有用,如果确定没有什么可以施放,则不必进行施放。
    • GC.Collect() 执行阻塞收集——该方法仅在收集完成后返回,因此对 GC.WaitForFullGCComplete() 的调用应该是不必要的。 GC.WaitForFullGCComplete() 旨在用于不同的场景。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-29
    • 2012-03-02
    • 2020-10-23
    • 1970-01-01
    • 2022-11-13
    • 2021-10-04
    相关资源
    最近更新 更多