我有一个已经通过了几个月又几个月(几年?)的单元测试,上周突然开始对我失败,仅适用于调试版本(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);
}
}
有什么作用?我给人的印象是,当对象的最后引用消失时,对象就变成了垃圾。我已经被几位作者警告过,只要在范围内 仍然引用它们,只要不再使用这些引用,这些对象就会变成垃圾。这个测试曾经起作用的事实表明我是对的。但是现在看来,由局部变量(至少)引用的对象直到包含该变量的函数结束才是垃圾。有什么变化吗?
答案 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,因为它会通过删除可能仍然会影响变量的值来阻碍调试(这是构建的全部要点)。当程序处于中断模式时,将由您检查。