代码之家  ›  专栏  ›  技术社区  ›  dlf

为什么在对该对象的唯一引用设置为空之后,该对象就没有变成垃圾?

  •  0
  • dlf  · 技术社区  · 6 年前

    我的单元测试已经通过了几个月(几年?)。上周我突然开始在调试版本中失败(Visual Studio 2017 15.7.5,.NET Framework 4.5)。它依赖一个本地变量引用的对象,在该变量设置为空之后,该对象将成为垃圾。我能够将内容提炼到下面(不需要测试框架):

    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 回复  |  直到 6 年前
        1
  •  1
  •   Theodoros Chatzigiannakis    6 年前

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

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

    在第二个断言上设置断点并获取内存快照表明内存中确实有一个foo对象,其根是“局部变量”。将前三行封闭在自己的一组 {} s没有任何区别,但是将它们提取到本地函数中可以通过测试。

    您创建的内部范围没有反映在CIL代码中,这就是为什么它没有任何区别。另一方面,当局部函数返回时,它的堆栈帧可能会被清理掉(除非其他机制也开始取消这种效果)。

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

    没有剩余任何可访问引用的对象可以进行垃圾收集,但实际收集它们的时间由GC决定。这尤其适用于 objects with finalizers ,在回收之前放入终结队列。

    有几位作者警告我,只要不再使用这些引用,对象在作用域中仍然有对它们的引用,它们就可能成为垃圾。

    当编译器能够证明对对象的其余引用不会被再次访问时,就会发生这种情况,即使它们在源代码的作用域中。决定这一点的运行时机制无论如何都没有源代码的概念。短暂变量(创建后仅使用一次)是此优化的主要候选者。

    但是,如果您正在调试配置下构建和运行程序,那么 the compiler and the runtime will refrain from making the aforementioned optimization ,因为它将通过删除程序处于中断模式时仍可以由您检查的变量值来阻碍调试(这是构建的整个点)。