垃圾回收应该已经删除对象,但WeakReference.IsAlive仍然返回true

15

我有一个测试,本来期望它能通过,但垃圾回收器的行为并不像我预想的那样:

[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对象应该被垃圾回收,因此我期望WeakReference.IsAlive属性返回false
似乎由于obj变量在与GC.Collect相同的作用域中声明,它没有被回收。如果我将obj的声明和初始化移到方法之外,测试就会通过。
有没有人对这种行为有任何技术参考文档或解释?

1
你检查过IL代码长什么样子吗?此外,发布版本和调试版本的行为是否相同? - Matthew Watson
3
我的初步猜测是编译器/运行时/处理器的优化正在影响你的代码。它们意识到你从未读取过 obj,所以允许在其他方法调用之间重新排序操作。尝试添加类似于 Console.WriteLine(obj == null) 的内容,以防止编译器这样做。 - Servy
1
这个示例在我的机器上运行良好。我使用Console.WriteLine来记录IsAlive参数,而不是Should() - JaredPar
4
值得注意的是,在该点上并不能保证对象没有任何强引用。您不应编写假定垃圾收集器会在任何给定时间杀死对象的代码。 - Jonathan Grynspan
1
使用此方法尝试回收所有无法访问的内存。 - user7116
显示剩余2条评论
6个回答

15
我遇到了和你一样的问题 - 在除 NCrunch (可能是其他工具) 以外的任何地方,我的测试都能通过。在使用SOS进行调试时,发现一个测试方法的调用栈中还有其他根源。我的猜测是它们是由代码插桩引起的,导致禁用了编译器优化,包括正确计算对象可达性的优化。
这里的解决方法非常简单 - 不要从进行垃圾回收及测试对象存活性的方法中持有强引用。可以通过一个微不足道的辅助方法轻松实现此目的。下面的更改使您的测试用例在最初失败的 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();
}

谢谢!一个非常优雅的解决方案。我也在使用 NCrunch。 - TechnoTone
1
这个解决方案对我有效,尽管我简化了它。我将“CallInItsOwnScope”操作放在一个单独的函数中,而不是lambda中,这使得强制GC能够按预期执行。 - Kent
我遇到了这个问题(使用MSTest进行驱动)-但只有在64位测试运行时才会出现。在32位下,尽管处于相同的作用域,GC仍然清理了对象。无法解释为什么会有所不同。 - Rags
在使用Xunit进行测试并在JetBrains Rider中运行时遇到了相同的问题。Lambda解决方案(以及Kent建议的单独函数解决方案)非常有效。谢谢! - Alexis

10

我能看到几个潜在的问题:

  • 我不知道C#规范中是否要求局部变量的生命周期受限。在非调试构建中,我认为编译器可以自由地忽略对obj的最后一次赋值(将其设置为null),因为没有任何代码路径会导致在此之后使用obj的值,但我期望在非调试构建中,元数据会指示该变量在创建弱引用后不再被使用。在调试构建中,变量应该存在于整个函数作用域中,但obj = null;语句实际上应该清除它。尽管如此,我不能确定C#规范是否承诺编译器不会省略最后一个语句,但仍然保留变量。

  • 如果你使用并发垃圾收集器,则GC.Collect()可能会触发立即开始收集,但在GC.Collect()返回之前,收集可能不会真正完成。在这种情况下,等待所有终结器运行可能是不必要的,因此GC.WaitForPendingFinalizers()可能过度了,但它可能会解决问题。

  • 当使用标准垃圾收集器时,我不希望弱引用到一个对象的存在会像终结器一样延长对象的存在时间,但当使用并发垃圾收集器时,存在弱引用的废弃对象可能会被移动到需要清理的带有弱引用的对象队列中,并且这种清理的处理在与其他所有内容并发运行的单独线程上进行。在这种情况下,调用GC.WaitForPendingFinalizers()将是实现所需行为的必要条件。

请注意,通常不应期望弱引用会在特定时间内失效,也不应期望在 IsAlive 报告为 true 后获取 Target 将产生非空引用。只有在不关心目标是否仍然存在,但想知道引用是否已死亡的情况下,才应使用 IsAlive。例如,如果有一组 WeakReference 对象,可能希望定期遍历列表并删除其目标已死亡的 WeakReference 对象。应该做好准备,即使 WeakReferences 在集合中停留的时间可能比理想情况下更长,如果确实如此,唯一的后果应该是轻微浪费内存和 CPU 时间。

4
据我所知,调用Collect并不能保证所有资源都被释放。这只是向垃圾回收器提出建议。
您可以尝试通过以下方式强制使其阻塞直到所有对象被释放:
GC.Collect(2, GCCollectionMode.Forced, true);

我认为这可能并不绝对有效。总的来说,我会避免编写任何依赖于垃圾回收观察的代码,因为它并不真正设计用于这种方式。


3

这个答案与单元测试无关,但对于正在测试弱引用并想知道为什么它们不能像预期那样工作的人可能会有所帮助。

问题基本上是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;,将会阻止该对象的垃圾回收。


2
< p > .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();
        }
    }
}

当您尝试此代码时会发生什么?

2
如果Should持有一个引用,它需要在静态变量中,否则它将超出范围并被垃圾回收。 - Servy
同意,这似乎不太可能。但是如果没有看到它的源代码,我必须假设这种可能性。虽然我认为这只是与您所建议的空引用何时变成垃圾可收集的时间有关的“未定义行为”。 - Matthew Watson
"Should"来自FluentAssertions库。使用Assert.False可以得到相同的行为。 - TechnoTone
谢谢 Matthew。我也确认这对我有效。所以这个问题确实是编译器优化的结果。不知道是否可以在测试期间禁用相关的优化,以便我可以创建一个真实的测试场景...? - TechnoTone
你的示例代码进行了一个看似不相关的小调整,但却无法正常工作:http://stackoverflow.com/questions/34399139/garbage-collection-being-successful-seems-to-depend-on-non-related-things - chtenb
我也看到上面的代码示例在我的计算机上适用于 .NET 4.5+,但不适用于更低版本。 - Thulani Chivandikwa

0

我有一种感觉,你需要调用GC.WaitForPendingFinalizers(),因为我预计弱引用将会在终结器线程中被更新。

很多年前我写单元测试的时候遇到过问题,回忆起来WaitForPendingFinalizers()有所帮助,同样的GC.Collect()也是。

该软件在现实生活中从未泄漏,但编写单元测试以证明对象没有被保留比我希望的更加困难。(我们过去在缓存方面存在错误,这些错误使其仍然存在。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接