让我先做一个演示:
[TestMethod]
public void Test()
{
var h = new WeakReference(new object());
GC.Collect();
Assert.IsNull(h.Target);
}
此代码按预期工作。垃圾回收结束后,h
中的引用将作废。现在,这里有一个转折点:
[TestMethod]
public void Test()
{
var h = new WeakReference(new object());
GC.Collect();
try { } // I just add an empty
finally { } // try/finally block
Assert.IsNull(h.Target); // FAIL!
}
我在GC.Collect()
行之后向测试添加一个空的 try/finally 块,瞧,没有收集弱引用的对象!如果在GC.Collect()
行之前添加了空的try/final块,则测试通过。
什么给?谁能解释一下尝试/最终块如何影响对象的生命周期?
注意:所有测试都在调试中完成。在"发布"中,两个测试都通过。
注意 2:若要重现应用,必须面向 .NET 4 或 .NET 4.5 运行时,并且必须以 32 位运行(目标 x86 或选中了"首选 32 位"选项的任何 CPU)
器时,抖动会改变局部变量的生存期。 在这个回答中详细解释。 简而言之,如果没有调试器,生存期在代码中最后一次使用变量时结束,使用调试器,生存期将扩展到方法末尾,以允许 Watch 调试器表达式正常工作。
虽然看起来new object()
表达式没有存储在代码中的变量中,但在抖动代码生成器使用它完成后仍然有一个。 对象引用存储在 [ebp-44h] 的堆栈帧上,与局部变量的使用方式没有区别。 您可以看到的唯一方法是查看生成的机器代码,使用调试 + Windows + 反汇编。 否则这是完全正常的,抖动优化器消除了这些冗余内存存储,但在调试版本中未启用。
即使它是临时变量,此变量仍需要作为存储引用报告给 GC。 这是防止在对象构造函数调用和 WeakReference 构造函数调用之间发生 GC 时收集对象所必需的。 如果程序中的另一个线程触发集合,则可能。
如果没有try/final块,抖动仍然可以发现堆栈帧插槽存储了一个临时的,实际上不需要延长其寿命。 因此,它会停止在 GC 之前报告临时的生存期。Collect() 调用并收集对象。
但是对于 try/finally 块,抖动放弃了尝试确定 try 或 finally 块中是否有堆栈帧插槽的可能使用。 并通过简单地将其生存期延长到方法的末尾来解决问题,就像普通局部变量一样。
这一切都是相当正常的,你根本无法对非优化代码中处理局部变量引用的方式做出任何合理的假设。 对于任何实际使用在单元测试器中运行的 [TestMethod] 的人来说,这也应该是一个强烈的警告,永远不要测试代码的调试版本,只测试发布版本。 它的行为与它在用户计算机上的工作方式不同。
为了便于调试,在调试模式下,不会释放本地声明的对象。虽然我无法重现您的问题,但使用以下代码:
var x = new object();
var h = new WeakReference(x);
GC.Collect();
try { } // I just add an empty
finally { } // try/finally block
Console.WriteLine(h.Target != null);
Console.ReadKey();
我能够重现该问题。如果GC.Collect()
能够"收集"new object()
,如果你在Console.ReadKey()
之后放置一个断点,你将能够看到一个已经处置的对象(x
)。
有人在这里问过类似的事情:https://stackoverflow.com/a/755688/613130
一条评论很有趣:
由于在发布模式下进行了优化,引用范围仅在最后一次使用之前有效,而不是在其定义的整个代码块中有效。
显然,在调试模式下情况正好相反,引用范围是定义它的整个范围。