15

我有一个我希望通过的测试,但垃圾收集器的行为并不像我想象的那样:

[Test]
public void WeakReferenceTest2()
{
    var obj = new object();
    var wRef = new WeakReference(obj);

    wRef.IsAlive.Should().BeTrue(); //passes

    GC.Collect();

    wRef.IsAlive.Should().BeTrue(); //passes

    obj = null;

    GC.Collect();

    wRef.IsAlive.Should().BeFalse(); //fails
}

在此示例中,obj对象应该是 GC'd,因此我希望该WeakReference.IsAlive属性返回false

似乎因为该变量被声明在与它没有被收集obj的相同范围内。GC.Collect如果我将 obj 声明和初始化移到测试通过的方法之外。

是否有人对此行为有任何技术参考文档或解释?

4

6 回答 6

15

遇到与您相同的问题-我的测试到处都通过,除了在 NCrunch 下(在您的情况下可能是任何其他仪器)。嗯。使用 SOS 进行调试会发现在测试方法的调用堆栈上保留了额外的根。我的猜测是,它们是禁用任何编译器优化的代码检测的结果,包括那些正确计算对象可达性的优化。

这里的解决方法很简单——永远不要从执行 GC 和测试活动的方法中获得强引用。这可以通过简单的辅助方法轻松实现。下面的更改使您的测试用例通过 NCrunch,它最初是失败的。

[TestMethod]
public void WeakReferenceTest2()
{
    var wRef2 = CallInItsOwnScope(() =>
    {
        var obj = new object();
        var wRef = new WeakReference(obj);

        wRef.IsAlive.Should().BeTrue(); //passes

        GC.Collect();

        wRef.IsAlive.Should().BeTrue(); //passes
        return wRef;
    });

    GC.Collect();

    wRef2.IsAlive.Should().BeFalse(); //used to fail, now passes
}

private T CallInItsOwnScope<T>(Func<T> getter)
{
    return getter();
}
于 2016-06-01T05:07:27.853 回答
10

我可以看到一些潜在的问题:

  • 我不知道 C# 规范中要求限制局部变量的生命周期的任何内容。在非调试版本中,我认为编译器可以自由地省略最后一次赋值obj(将其设置为null),因为没有代码路径会导致obj在它之后永远不会使用的值,但我希望在非debug build 元数据将表明该变量在创建弱引用后从未使用过。在调试版本中,变量应该存在于整个函数范围内,但obj = null;语句实际上应该清除它。尽管如此,我不确定 C# 规范是否承诺编译器不会省略最后一条语句,但仍保留变量。

  • 如果您正在使用并发垃圾收集器,则可能会GC.Collect()触发收集的立即开始,但收集实际上不会在GC.Collect()返回之前完成。在这种情况下,可能没有必要等待所有终结器运行,因此GC.WaitForPendingFinalizers()可能有点矫枉过正,但它可能会解决问题。

  • 使用标准垃圾收集器时,我不希望对象的弱引用以终结器的方式延长对象的存在,但是当使用并发垃圾收集器时,可能会丢弃对象存在的弱引用被移动到具有需要清理的弱引用的对象队列中,并且此类清理的处理发生在与其他所有内容同时运行的单独线程上。在这种情况下,需要调用来GC.WaitForPendingFinalizers()实现所需的行为。

请注意,通常不应期望弱引用会因任何特定程度的及时性而失效,也不应期望在报告为真Target后获取IsAlive将产生非空引用。仅应IsAlive在不关心目标是否还活着但有兴趣知道引用已死的情况下使用。例如,如果一个人有一组WeakReference对象,可能希望周期性地遍历列表并删除WeakReference目标已经死亡的对象。应该为WeakReferences可能在收藏中保留的时间超过理想情况下的可能性做好准备;如果他们这样做,唯一的后果应该是稍微浪费内存和 CPU 时间。

于 2013-03-04T16:37:31.330 回答
4

据我所知,调用Collect并不能保证所有资源都被释放。您只是向垃圾收集器提出建议。

您可以尝试强制它阻塞,直到所有对象都被释放:

GC.Collect(2, GCCollectionMode.Forced, true);

我预计这可能不会在 100% 的时间内绝对有效。一般来说,我会避免编写任何依赖于观察垃圾收集器的代码,它并不是真正设计用于以这种方式使用的。

于 2013-03-04T16:36:00.377 回答
2

可能是.Should()扩展方法以某种方式挂在引用上吗?或者也许是测试框架的其他方面导致了这个问题。

(我将此作为答案发布,否则我无法轻松发布代码!)

我尝试了以下代码,它按预期工作(Visual Studio 2012、.Net 4 构建、调试和发布,32 位和 64 位,在 Windows 7 上运行,四核处理器):

using System;

namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var obj = new object();
            var wRef = new WeakReference(obj);

            GC.Collect();
            obj = null;
            GC.Collect();

            Console.WriteLine(wRef.IsAlive); // Prints false.
            Console.ReadKey();
        }
    }
}

当您尝试此代码时会发生什么?

于 2013-03-04T16:20:01.513 回答
1

这个答案与单元测试无关,但它可能对测试弱引用并想知道为什么它们不能按预期工作的人有所帮助。

问题基本上是让变量保持活动状态的 JIT。这可以通过在非内联方法中实例化 WeakReference 和目标对象来避免:

private static MyClass _myObject = new MyClass();

static void Main(string[] args)
{
    WeakReference<object> wr = CreateWeakReference();

    _myObject = null;

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

    wr.TryGetTarget(out object targetObject);

    Console.WriteLine(targetObject == null ? "NULL" : "It's alive!");
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static WeakReference<object> CreateWeakReference()
{
    _myObject = new MyClass();
    return new WeakReference<object>(_myObject);
}

public class MyClass
{
}                                                            

注释掉_myObject = null;将阻止该对象的垃圾收集。

于 2021-08-07T23:31:47.513 回答
0

我有一种感觉,您需要调用GC.WaitForPendingFinalizers(),因为我希望终结器线程会更新周引用。

多年前,我在编写单元测试时遇到了问题,并回忆起这有WaitForPendingFinalizers()帮助,因此调用GC.Collect().

该软件在现实生活中从未泄漏,但编写单元测试以证明该对象没有保持活动状态比我希望的要困难得多。(过去我们的缓存存在错误,但确实保持了它的活力。)

于 2017-02-08T15:54:49.247 回答