在仅对该对象的引用设置为null之后,为什么该对象没有变成垃圾?

时间:2018-07-30 13:57:29

标签: .net garbage-collection visual-studio-2017

我有一个已经通过了几个月又几个月(几年?)的单元测试,上周突然开始对我失败,仅适用于调试版本(Visual Studio 2017 15.7.5,.net framework 4.5)。它依赖于由局部变量引用的对象,该对象在该变量设置为null之后变为垃圾。我能够将内容归纳为以下内容(无需测试框架):

private class Foo
{
    public static int Count;
    public Foo() => ++Count;
    ~Foo() => --Count;
}

public void WillFail()
{
    var foo = new Foo();
    Debug.Assert(Foo.Count == 1);
    foo = null;
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Debug.Assert(Foo.Count == 0);
}

在第二个断言上设置断点并进行内存快照显示,内存中确实存在一个Foo对象,并且其根是“局部变量”。将前三行包含在它们自己的{}中并没有什么区别,但是将它们提取到本地函数中可以使测试通过:

public void WillPass()
{
    DoIt();
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Debug.Assert(Foo.Count == 0);

    void DoIt()
    {
        var foo = new Foo();
        Debug.Assert(Foo.Count == 1);
    }
}

有什么作用?我给人的印象是,当对象的最后引用消失时,对象就变成了垃圾。我已经被几位作者警告过,只要在范围内 仍然引用它们,只要不再使用这些引用,这些对象就会变成垃圾。这个测试曾经起作用的事实表明我是对的。但是现在看来,由局部变量(至少)引用的对象直到包含该变量的函数结束才是垃圾。有什么变化吗?

1 个答案:

答案 0 :(得分:1)

  

我有一个已经通过了几个月又几个月(几年?)的单元测试,上周突然开始对我失败(Visual Studio 2017 15.7.5,.net framework 4.5)。它依赖于由局部变量引用的对象,该对象在该变量设置为null后变为垃圾。

您永远都不要在生产代码中依赖它,因为无论您的特定测试是否通过,.NET GC都可以完全按照指定的方式工作。

  

在第二个断言上设置断点并进行内存快照,这表明内存中确实存在一个Foo对象,其根是“局部变量”。将前三行括在自己的{}组中并没有什么区别,但是将它们提取到局部函数中可以使测试通过。

您创建的内部作用域未反映在CIL代码中,这就是为什么它没有区别的原因。另一方面,局部函数可能会在返回时清除其堆栈框架(除非也有其他机制取消这种效果)。

  

我的印象是,当最后一次引用对象消失时,对象就变成了垃圾

没有任何可访问引用的对象可以进行垃圾回收,但是GC决定何时实际收集它们。对于objects with finalizers来说尤其如此,the compiler and the runtime will refrain from making the aforementioned optimization在回收之前已放入终结队列中。

  

我已经被多位作者警告过,只要范围内仍然没有对这些引用的引用,对象可能会变成垃圾。

当编译器可以证明不会再次访问该对象的其余引用(即使它们在源代码的范围内)时,就会发生这种情况。决定这一点的运行时机制始终没有源代码的概念。临时变量(创建后仅使用一次)是此优化的主要候选对象。

但是,如果要在Debug配置下构建和运行程序,则PHP.net docs,因为它会通过删除可能仍然会影响变量的值来阻碍调试(这是构建的全部要点)。当程序处于中断模式时,将由您检查。