.NET服务器垃圾回收和对象生命周期

8

我和同事对于.NET中一个对象何时可以被垃圾回收存在分歧。看下面的代码:

Stream stream=getStream();
using(var request=new Request(stream))
{
    Stream copy=request.Stream;

    // From here on can "request" be garbage collected?

    DoStuff1();
    DoStuff2(copy);
}

我的同事声称,当使用服务器垃圾回收器运行发布构建时,调用request.Stream后,request对象可以被垃圾回收。他断言,这只会发生在使用服务器垃圾回收器时,而永远不会发生在工作站垃圾回收器上。
原因是Request类有一个终结器,该终结器正在关闭提供给该请求的Stream。因此,当DoStuff2使用该流进行操作时,它会得到一个“对象已释放”异常。由于终结器只能由垃圾回收器运行,我的同事说,在finally块结束之前,必须进行了垃圾回收,但在最后使用request之后。
然而,我认为由于上面的代码只是以下内容的缩写:
Stream stream=getStream();
Request request=null;

try
{
    Stream copy=request.Stream;

    // From here on can "request" be garbage collected?

    DoStuff1();
    DoStuff2(copy);
}
finally
{
    if(request!=null)
        request.Dispose();
}

那么,由于在finally块中仍然可以访问request对象,因此在调用request.Stream后,request将无法被垃圾回收。

另外,如果垃圾回收器能够收集该对象,则finally块可能会表现出未定义的行为,因为会对已经被垃圾回收的对象调用Dispose,这是没有意义的。同样地,不可能优化掉finally块,因为在进行任何垃圾回收之前,try/using块中可能会抛出异常,这将需要执行finally块。

忽略在终结器中关闭流的问题,是否有可能在finally块结束之前垃圾回收器收集该对象,并实际上优化掉finally块中的逻辑?


2
你是正确的,请求在 finally 运行之前不能被 GC 回收。 - Ben Robinson
1
请注意,如果 request.Dispose 没有使用实例字段,则在调用 Dispose 之前可以收集 request。这是一种罕见的情况。 - usr
@usr - 当然可以。但是我们的 Dispose 方法正在执行一些操作! - Sean
你们都错了,ODE 永远 与 GC 没有任何关系。已回收的对象无法引发异常。这通常是你的代码中所有权问题的 bug,在关闭流之后继续使用它。这样的 bug 在片段中非常明显,由 getStream() 返回的流将在此代码运行完成后被处理。这种情况通常是不正确的。 - Hans Passant
1
@HansPassant - 我理解了。我的问题是,是否存在一种场景或优化,使垃圾收集器在using/finally块结束之前GC request对象?ODE是由Request对象中糟糕编码的终结器引起的,但我的观点是它不应该在using块结束之前运行终结器,而且只有在Dispose之后才能运行。 - Sean
显示剩余6条评论
2个回答

10

这个问题涉及的内容比较多,我先处理一下总体问题。

  1. using语句中声明的变量会在块结束前不会被垃圾回收,正如你所说 - 为了在隐式的finally块中调用Dispose()而持有引用。

  2. 如果你在C#中编写析构函数,那么你可能做错了什么。如果你在C#中的析构函数中调用Stream.Dispose(),那么你肯定做错了什么。除了.NET本身的实现之外,我见过数百个误用的析构函数,但只有一个真正需要的析构函数。更多信息请参见DG更新:Dispose、Finalization和资源管理

  3. ObjectDisposedException与析构函数无关。当代码调用对象的Dispose()(而不是析构函数),然后稍后再调用该对象时,通常会发生此异常。

    有时候不容易察觉到代码正在处理一个Stream。一个让我感到惊讶的情况是,在使用HttpClient发送HTTP请求时,使用StreamContent。该实现在发送完请求后调用Stream.Dispose(),因此我不得不编写一个包装器Stream类,称为DelegatingStream,以在从HttpWebRequest转换为HttpClient过程中保留我们库的原始行为。

    1. 如果getStream()方法缓存了一个Stream实例并在未来的多次调用中返回它,则可能会出现ObjectDisposedException。如果Request.Dispose()处理流,或者DoStuff2(Stream)处理流,则下一次尝试使用该流时,将会抛出ObjectDisposedException

绝对正确。对象处理是框架实现的一种模式,允许轻松等待对象释放资源,并不直接绑定到GC。唯一的限制是Disposed对象通常会释放资源,这些资源可以用于删除。在Stream中,您需要检查每种情况的实现以查看发生了什么。 - Miguel

7
根据语言规范第3.9节:“如果对象或其任何部分不能通过执行的任何可能的延续访问,除了运行析构函数之外,该对象被认为不再使用,并且变得适合销毁。”使用usingfinally块中引入一个Dispose调用(第8.13节),这与运行析构函数不同,并且必须执行,除非在甚至finally块都不执行的情况下(规范没有涵盖,通常只发生在整个AppDomain正在走向墓地的时候)。 using块中的对象不适合进行GC。在您的示例中,requestusing语句之后才适合销毁,无论它是否在块的其余部分中使用。唯一的特例(正如其他人在评论中提到的)是当您的Dispose实现不使用其this参数时。在这种情况下,Dispose将不会防止对象不适合销毁 - 但如果发生这种情况,则可以收集它的时间无关紧要(因为它应该是这样的 - 当正确实现程序时,垃圾收集不应对正确实现程序产生任何可观察的影响)。

1
我曾认为using语句创建了一个弱引用,但在语言规范中没有看到任何相关内容。确保变量可以被垃圾回收的方法是调用GC.ForceGC 两次。第一次调用将导致Dispose方法被调用。第二次调用将导致对象内存被释放。 - KC-NH
@KC-NH:没有GC.ForceGC方法。您可以通过实现终结器、设置外部标志并调用GC.Collect(),然后调用GC.WaitForPendingFinalizers()来人为地检测对象是否符合销毁条件。显然,这不是您想在生产代码中执行的操作,因此它只有教学价值。如果您尝试对using中的变量进行此操作,您将看到它在using完成之前无法被回收。在我的测试中,即使Dispose()根本不访问任何成员,这也是正确的。 - Jeroen Mostert
抱歉,我真正想说的是GC.Collect。我会通过让Dispose()记录它被调用来进行测试。如果你有一个终结器,请记录它也被调用了。 - KC-NH
@KC-NH:小心不要混淆Dispose()和终结器——它们完全没有关系。如果您调用了Dispose(),则会在调用时调用它——最好通过using来调用。如果实现了终结器,则由终结线程调用。如果您分配了一个对象但从未调用Dispose()(也没有终结器),那么Dispose()就不会被调用——运行时不认为它有什么特殊之处。话虽如此,大多数同时具有两者的实现确实会从终结器中调用Dispose() - Jeroen Mostert
你说得对。我相信设计模式是在Dispose()中释放资源,并从finalized中调用Dispose()。我忘记了GC不识别IDisposable。这是我每次需要时都要查找的模式之一。 - KC-NH

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