我有一个我希望通过的测试,但垃圾收集器的行为并不像我想象的那样:
[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声明和初始化,则测试通过。
是否有人对此行为有任何技术参考文档或解释?
答案 0 :(得分:10)
我可以看到一些潜在的问题:
我不知道C#规范中的任何内容要求局部变量的生命周期是有限的。在非调试版本中,我认为编译器可以自由地省略obj
的最后一个赋值(将其设置为null
),因为没有代码路径会导致obj
的值永远不要在它之后使用,但我希望在非调试版本中元数据表明在创建弱引用之后永远不会使用该变量。在调试版本中,变量应该存在于整个函数范围内,但obj = null;
语句应该实际清除它。尽管如此,我不确定C#规范承诺编译器不会省略最后一个语句但仍保留变量。
如果您正在使用并发垃圾收集器,那么GC.Collect()
可能会触发集合的立即启动,但在GC.Collect()
返回之前实际上不会完成收集。在这种情况下,可能没有必要等待所有终结器运行,因此GC.WaitForPendingFinalizers()
可能过度,但它可能会解决问题。
当使用标准垃圾收集器时,我不希望存在对象的弱引用以终结器的方式延长对象的存在,但是当使用并发垃圾收集器时,它是可能存在弱引用的被放弃对象被移动到具有需要清理的弱引用的对象队列,并且这种清理的处理发生在与其他所有内容同时运行的单独线程上。在这种情况下,需要调用GC.WaitForPendingFinalizers()
才能实现所需的行为。
请注意,通常不应期望弱引用会因任何特定程度的及时性而失效,也不应期望在Target
报告为true后获取IsAlive
将产生非空引用。一个人应该只在一个人不关心目标的情况下使用IsAlive
,如果它仍然存在,但是有兴趣知道该引用已经死亡。例如,如果有一个WeakReference
个对象的集合,则可能希望定期遍历列表并删除目标已经死亡的WeakReference
个对象。应该为WeakReferences
可能在集合中保留的时间超过理想必要的时间做好准备;如果他们这样做的唯一后果应该是略微浪费内存和CPU时间。
答案 1 :(得分:10)
遇到与你相同的问题 - 我的测试在任何地方传递,除了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();
}
答案 2 :(得分:4)
据我所知,调用Collect
并不保证所有资源都已释放。你只是向垃圾收集器提出建议。
您可以尝试强制它阻止,直到所有对象都被释放为止:
GC.Collect(2, GCCollectionMode.Forced, true);
我希望这可能无法在100%的时间内完全发挥作用。一般来说,我会避免编写任何依赖于观察垃圾收集器的代码,它实际上并不是以这种方式使用。
答案 3 :(得分: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();
}
}
}
尝试此代码时会发生什么?
答案 4 :(得分:0)
我觉得您需要调用GC.WaitForPendingFinalizers(),因为我希望终结器线程更新周引用。
多年前我在编写单元测试并回忆起WaitForPendingFinalizers()
有所帮助时遇到了问题,拨打GC.Collect()
也是如此。
软件从未在现实生活中泄露过,但是编写一个单元测试来证明对象没有保持活着比我希望的要困难得多。 (过去我们的缓存存在错误,确实让它保持活力。)
答案 5 :(得分:0)
这个答案与单元测试无关,但它可能对正在测试弱引用并想知道它们为什么不能按预期工作的人有所帮助。
问题基本上是 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;
将阻止对该对象进行垃圾回收。