垃圾收集器不会回收使用using创建的对象

6

我想测试对象引用是否不正确,并编写了一个总是失败的测试。我将测试简化为以下行为:

    [Test]
    public void ScopesAreNotLeaking()
    {
        WeakReference weakRef;
        Stub scope = null;
        using (scope = new Stub())
        {
            weakRef = new WeakReference(scope);
        }
        scope = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Assert.That(weakRef.Target, Is.Null);
    }

然而,这个测试用例在不使用该功能的情况下也能通过:

    [Test]
    public void ScopesAreNotLeaking()
    {
        WeakReference weakRef;
        Stub scope = new Stub();
        weakRef = new WeakReference(scope);
        scope = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Assert.That(weakRef.Target, Is.Null);
    }

使用的存根类足够简单:
class Stub : IDisposable
{
    public void Dispose()  {}
}

有人能解释一下这种行为吗,或者更好的是,有什么办法可以确保该对象被垃圾收集?

附注:如果之前已经有类似的问题,请见谅。我只查看了标题中包含使用的那些问题。


1
我怀疑在using语句中引入了一个本地变量。使用ildasm查看函数中对该对象的所有引用是否在调用GC.Collect之前真正清除。还可以尝试将using位放在一个单独的函数中,该函数返回弱引用。 - Sven
@Sven:你做对了,将using语句放在单独的方法中可以让测试通过。现在,如果你写一个答案,我就可以标记它了... - MZywitza
谢谢,我将我的评论复制粘贴到了答案中。 :) - Sven
尽管每个Web请求只会创建少量对象,但它们持有对许多其他对象的引用,这些对象由于此原因不会被GC回收。Scope对象由单例管理器暂时保存。如果它们没有从管理器对象中释放,堆将增长超过给定限制。 - MZywitza
尽管我不了解你具体的应用程序或应用程序背后的设计,但我会说如果这真的是个问题,你引用了太多的对象。 - thekip
显示剩余4条评论
4个回答

5

using并不是为了强制垃圾回收,而是为了确保调用dispose方法。Dispose允许您释放非垃圾回收的资源,例如文件句柄。垃圾回收发生在C#准备好时。


@codymanix:只是开玩笑,我只是想说垃圾回收的时间是不确定的。 - Patrick
这并没有回答问题。问题是:“在第二个例子中,如果没有使用using,存根将在GC.Collect调用后被收集,为什么在第一个情况下不是呢?”为什么使用using会产生任何影响?(它似乎确实有影响,已经运行了代码) - Rob Levine
@Rob Levine:你必须充分理解GC才能理解为什么一个对象会被回收或不会被回收。对我来说,重要的一点是GC是非确定性的,因此您不能依赖第二个测试通过。它适用于OP现在使用的框架版本,但可能无法在不同版本或不同程序中运行。 - Patrick
我认为OP的问题(或者当我阅读它时我的问题)更多是“似乎使用导致对象无法进行垃圾回收,就像有一个隐藏的强引用”。不过我不确定你的确定性观点。对于给定的GC,GC的行为是确定性的,不是吗?(例如,在发生gen3 GC时它会做什么)。它是不确定的,因为你不知道它何时选择收集和哪一代,但在这种情况下它已经被强制了。我可能错了,但无论如何,OP发现的好奇心仍然存在。 - Rob Levine

2

我怀疑在using语句中引入了一个本地变量。使用ildasm查看函数中对该对象的所有引用是否在调用GC.Collect之前被真正清除。还可以尝试将using语句放在一个单独的函数中,并返回弱引用。


using() 不会生成仍保留在 using 语句外部范围内的附加局部变量: http://msdn.microsoft.com/zh-cn/library/aa664736(v=vs.71).aspx - codymanix
它们不在 C# 代码的范围内;但从 MSIL 的角度来看,所有本地变量都会一直存在,直到函数结束,因此如果它们没有被清空,GC 就不会收集对象。 - Sven
我们中的几个人仔细研究了IL代码,发现这确实是正确的。在 using 语句块中创建了另一个本地变量(因此您最终会得到对stub的两个引用),其中只有一个被释放。 - Rob Levine

1

像在.NET中使用的标记和清除GC一样,它不是确定性的。 即使通过GC.Collect()调用,也不能保证真正运行。

此外,using语句与垃圾回收无关。它只调用其目标对象上的Dispose()方法。


你能详细说明一下吗?虽然它不是确定性的,但我相信编写它的开发人员可以告诉你何时决定进行收集的算法。我很想知道:p。 - Robinson
此外,using语句与垃圾回收无关。它只是在目标对象上调用Dispose()方法。这就是OP的观点。为什么using会对GC产生影响?如果您运行OP的代码,您将看到所描述的行为确实发生了-问题是为什么。 - Rob Levine
如果多次运行或从调试更改为发布配置,结果不同也不会让我感到惊讶。使用()会创建一个finally子句,谁知道这会如何影响本地变量。 - codymanix

1

你的两个测试用例并不相同。在 using 语句的结尾处,资源的 Dispose 方法会被调用,而不是设置为 null。调用 Dispose 并不一定会调用析构函数。


在.NET中有一个名为构造函数的东西。顺便说一下,在这两个例子中,作用域都设置为null。 - codymanix
从什么时候开始?我没有提到构造函数,但是C#有构造函数(http://msdn.microsoft.com/en-us/library/ace5hbzh.aspx)和析构函数(http://msdn.microsoft.com/en-us/library/66x5fx1b.aspx)。 - Shea Levy

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