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