如果我不调用笔对象的Dispose方法会发生什么?

41

如果我在这段代码片段中没有调用 pen 对象的 Dispose 方法,会发生什么?

private void panel_Paint(object sender, PaintEventArgs e)
{
    var pen = Pen(Color.White, 1);
    //Do some drawing
}

36
没有什么。这就是为什么你应该打电话的原因。 - user395760
在我看来,MSDN文档中关于该方法的说明非常清晰。http://msdn.microsoft.com/en-us/library/system.drawing.pen.dispose(v=VS.90).aspx - Captain Sensible
@Bryan 我知道我应该调用Dispose,但我想了解为什么。如果调用它非常重要,为什么不自动执行? - Andreas Brinck
7
.NET不进行逃逸分析,因此它不知道引用是否在pen超出作用域后仍然存在。因此,您必须等待垃圾回收器决定何时收集可能晚得多的Pen - CodesInChaos
1
如果您懒得输入 myobj.Dispose(),那么您必须使用 using 语句 来包装您的代码。 - Cheng Chen
显示剩余8条评论
10个回答

35

这里需要进行一些更正:

关于Phil Devaney的回答:

"...调用Dispose()方法可以进行可确定的清理,强烈推荐。"

实际上,在.NET中调用Dispose()方法并不会强制进行GC垃圾回收,即仅仅因为调用了Dispose()方法并不意味着立即触发GC回收操作。它只是间接地向GC发出信号,表示该对象可以在下一次GC(对于对象所处的代)进行清理。换句话说,如果该对象存在于第一代中,则它直到进行第一代垃圾回收才会被清理。虽然可以通过调用GC.Collect()方法来以编程方式和确定性地触发GC垃圾回收操作(尽管这不是唯一的方式),但不建议这样做,因为GC会根据运行时对应用程序内存分配的指标进行自我“调优”。调用GC.Collect()方法将导致这些指标被清除,并导致GC重新开始“调优”过程。

关于回答:

IDisposable用于释放非托管资源。这是.NET中的模式。

这个说法不完全正确。由于GC具有非确定性,因此Dispose模式(如何正确实现Dispose模式)可用于释放您正在使用的资源,无论是托管还是非托管。它与您要释放的资源类型没有任何关系。实现Finalizer是否必要则取决于您使用的资源类型-即仅在具有非可终结(即本地)资源时才实现Finalizer。也许您会混淆这两个概念。顺便提一下,您应该使用SafeHandle类来避免实现Finalizer,该类包装通过P/Invoke或COM互操作进行封送的本地资源。如果确实需要实现Finalizer,则始终应实现Dispose模式。

还有一点需要注意并且我还没有看到任何人提到,那就是如果创建了可清理对象并且它具有终结器(你永远不知道它们是否有 - 你肯定不应该对此做任何假设),那么它将直接发送到终结器队列,并至少存在一个额外的垃圾收集。

如果最终未调用GC.SuppressFinalize(),则对象的终结器将在下一次垃圾回收时调用。请注意,Dispose模式的适当实现应调用GC.SuppressFinalize()。因此,如果您在对象上调用Dispose(),并且它已正确实现该模式,则可以避免执行终结器。 如果您没有调用具有终结器的对象上的Dispose(),则对象将由垃圾回收器在下一次收集时执行其终结器。为什么这很糟糕?CLR中的终结器线程一直单线程,包括.NET 4.6以前。想象一下如果您增加了对此线程的负担会发生什么- 应用程序性能会急剧下降。

在对象上调用Dispose提供以下功能:

  1. 减轻进程的GC负担;
  2. 减少应用程序的内存压力;
  3. 如果LOH(大对象堆)被分段并且对象位于LOH上,则减少OutOfMemoryException(OOM)的机会;
  4. 如果具有终结器,则使对象保持不在可终结队列和f-reachable队列中;
  5. 确保清理您的资源(托管和非托管)。

编辑: 我刚才注意到“无所不知且总是正确”的MSDN文档关于IDisposable(极其讽刺)实际上说

此接口的主要用途是释放非托管资源

众所周知,MSDN远非正确,从未提到或展示“最佳实践”,有时提供的示例没有编译。不幸的是,这些话记录在档案中。但是,我知道他们想说什么:在一个完美的世界里,GC将为您清理所有托管资源(多么理想化);但它不会清理非托管资源。这是绝对真实的。话虽如此,生活并不完美,任何应用程序也都不是。 GC只会清理没有根引用的资源。 这通常是问题所在。

在大约15-20种.NET可能“泄漏”(或不释放)内存的方式中,如果不调用Dispose()最可能影响你的是未注销/取消挂钩/解除连接事件处理程序/委托。如果创建了一个对象,并将委托连接到它上面,而您不调用Dispose()(也不自己分离委托),则GC仍将视该对象具有根引用 - 即委托。因此,GC永远不会对其进行收集。

@ joren的以下评论/问题(我的回复太长无法作为评论):

我写了一篇关于Dispose模式的博客文章,我建议使用它 - (如何正确实现 Dispose 模式)。有时候您应该将引用设置为 null,这样做永远不会有坏处。实际上,在垃圾回收运行之前,设置为 null 确实会做一些事情 - 它会删除对该对象的根引用。稍后,垃圾回收器会扫描其根引用的集合,并收集那些没有根引用的对象。当您应该这样做时,请考虑以下示例:您有一个类型为“ClassA”的实例 - 让我们称其为 'X'。 X 包含一个类型为“ClassB”的对象 - 让我们称其为 'Y'。 Y 实现了 IDisposable,因此,X 也应该这样做以处理 Y 的释放。假设 X 在第二代或 LOH 中,而 Y 在第 0 或 1 代中。当在 X 上调用 Dispose() 并且该实现将对 Y 的引用设置为 null 时,对 Y 的根引用立即被删除。如果 Generation 0 或 Generation 1 进行 GC,则会清除 Y 的内存/资源,但不会清除 X 的内存/资源,因为 X 存在于第二代或 LOH 中。


2
关于明显的矛盾,Jason 表示:“IDisposable 用于处理非托管资源的清理”。它的唯一目的是确定性地清理资源 - 无论它们是否受到管理。正确实现 Dispose 模式可以让您清理资源。我并不是说未受管理的资源没有被清理,而是 IDisposable 的目的与您正在处理的资源类型无关。从他的陈述中可以推断出,除非您使用本机资源,否则不需要实现 IDisposable,这是绝对不正确的。 - Dave Black
2
我认为可以安全地假设如果实现了IDisposable接口,那么它具有需要以确定的方式释放的资源。无论是否存在非托管资源都是实现细节。 - davidcarr
3
@davidcarr - 我觉得你完全误解了我的回答的要点。在托管代码中避免调用Dispose是不合适的。我已经详细解释了为什么这是必要的。底线是:如果一个类实现了IDisposable接口,那么你应该调用它。因为依赖于某个类的内部实现细节来说你无需调用Dispose是一种糟糕的设计,这些实现细节应该被视为黑盒子——比如MemoryStream,以及任何其他.NET Framework(或其他外部框架)的类。内部实现是会发生变化的。 - Dave Black
3
@davidcarr - IDisposable模式被误解的一个原因是像你这样的人想要关注底层资源是否使用托管或非托管资源,好像这是决定性因素一样,但实际上不应该是决定性因素。如果你坚持“如果它实现了IDisposable接口,我就应该调用Dispose()”这个想法,那么你在任何情况下都会做得很好。如果人们遵循这个建议,模式就不会有太多的困惑。 - Dave Black
2
@davidcarr,我说“像你这样的人”时并没有任何不敬之意。我只是在描述。 - Dave Black
显示剩余13条评论

29
无论您是否调用 Dispose 方法,Pen 对象都将在未来某个不确定的时间被 GC(垃圾回收器)回收。
但是,由 Pen 持有的任何非托管资源(例如 GDI+ 句柄)将不会被 GC 清理。GC 仅清理托管资源。调用 Pen.Dispose 方法可确保及时清理这些非托管资源,以防资源泄漏。
如果 Pen 有一个终结器并且该终结器清理了非托管资源,则当 GC 回收 Pen 时,这些非托管资源将被清理。但重点是:
  1. 您应该显式调用 Dispose 以释放您的非托管资源,并且
  2. 您不需要关心是否存在终结器以及它是否清理了非托管资源的实现细节。
Pen 实现了 IDisposable 接口。 IDisposable 用于释放非托管资源。这是 .NET 中的惯用做法。
有关此主题的先前评论,请参见此答案

2
这个答案不完整且有些误导性。我在下面的帖子中解释了原因。 - Dave Black
4
-1 表示 GC 不会清理非托管资源的断言。如果 IDisposable 实现正确,它将在终结线程中正常工作,只是稍晚一些。 - Dominic Cronin
3
无论Dispose()如何实现,GC都不会释放非托管内存或资源。它可以让您调用Marshal.ReleaseComObject()并实现一个Finalizer,如果未调用GC.SuppressFinalize(),则会调用它。即使在终结或Dispose实现期间清理非托管内存,也不意味着是GC进行释放。GC不负责清理非托管资源,如果程序员从未这样做,那么内存将泄漏。 - Dave Black
@DaveBlack MSDN文档(http://msdn.microsoft.com/en-us/library/system.drawing.pen.dispose.aspx)上对Pen的描述与您的观点不同。"在释放对Pen的最后引用之前,始终调用Dispose。否则,它正在使用的资源将不会被释放,直到垃圾回收器调用Pen对象的Finalize方法。"除非编写得很糟糕,否则我认为“资源”是所有资源,包括未托管的资源。因此,在这种情况下,GC确实会释放未托管的资源。 - Tom
@Tom - GC 对非托管资源一无所知:不知道如何分配它们,也不知道如何释放它们。语言提供的是通过 Dispose 和 Finalizer 自行处理的手段。严格来说,Pen 类并不是非托管资源;而是一个非托管资源的托管包装器。Pen 类中的底层代码实现了 Finalizer。该类中的 Finalizer 执行必要的清理任务,因此在处理托管对象时,资源通常不会“泄漏”(极少数情况除外)。 - Dave Black

13

在未来的某个不确定时间,即当 Pen 对象被垃圾回收并调用对象的终结器时,底层 GDI+ 笔柄将不会被释放。这可能直到进程终止才会发生,或者可能会更早,但关键是它是不确定性的。调用Dispose允许您进行确定性清理,并且强烈建议这样做。


这个答案略有不正确。我在下面的帖子中解释了原因。 - Dave Black

2
如果你想知道不调用图形对象的Dispose方法有多糟糕,可以使用CLR Profiler进行分析。你可以在这里免费下载它:https://clrprofiler.codeplex.com/。在安装文件夹(默认为C:\CLRProfiler)中,有一个名为CLRProfiler.doc的文档,其中详细说明了不调用Brush对象的Dispose方法会发生什么。非常有启发性。简而言之,图形对象占用的内存比你想象的要大得多,并且如果你不调用Dispose方法,它们可能会持续存在很长时间。一旦这些对象不再使用,系统最终会清理它们,但这个过程所需的CPU时间比你在完成对象后直接调用Dispose方法要多。你也可以在这里点击这里阅读有关使用IDisposable的更多信息。

2
使用的.NET内存总量是.NET部分和所有正在使用的“外部”数据之和。操作系统对象、打开的文件、数据库和网络连接等都需要一些资源,这些资源不是纯粹的.NET对象。
图形使用笔和其他对象,它们实际上是操作系统对象,保存它们非常昂贵。(您可以将笔交换为1000x1000位图文件)。这些操作系统对象只有在调用特定的清理函数时才会从操作系统内存中删除。当您调用Pen和Bitmap Dispose函数时,它们会立即为您执行此操作。
如果您不调用Dispose,则垃圾回收器将在“未来的某个地方”清理它们。(它实际上会调用析构函数/终结代码,该代码可能调用Dispose())。
*在具有无限内存(或超过1GB)的机器上,“未来的某个地方”可能非常遥远。在空闲的机器上,清理那个巨大的位图或非常小的笔可能很容易超过30分钟。

1

它会保留资源,直到垃圾回收器清理它。


1
嗡!句柄将保持到进程终止。 - Aliostad
2
笔的终结器应该在运行时清理资源。 http://msdn.microsoft.com/en-us/library/system.drawing.pen.dispose.aspx 当然,不能保证它会运行。 - jk.
4
@Mitch、@Aliostad、@jk - 所有扩展自 Component 的对象都实现了一个将调用 Dispose 的终结器。该终结器是非确定性的,这意味着您无法预测何时它会运行,但它最终总会运行。如果另一个终结器无限期地阻塞或进程被终止,则当然不会运行。 - ChaosPandion
@ChaosPandion:在未捕获异常后卸载AppDomain时,普通的终结器不会被调用。为此您需要一个关键终结器。但我猜Pen使用safehandle并且因此进行了关键终结,但我没有验证过。 - CodesInChaos

1
取决于它是否实现了finalizer并在其finalize方法中调用了Dispose。如果是这样的话,在垃圾回收时将释放句柄。
如果没有这样做,句柄将一直保留,直到进程终止。

在释放对笔的最后引用之前,始终调用Dispose。否则,它正在使用的资源将不会被释放,直到垃圾回收器调用Pen对象的Finalize方法。 - J D

0

涉及图形处理时可能会出现很糟糕的情况。

打开Windows任务管理器。点击“选择列”,选择名为“GDI对象”的列。

如果您不处理某些图形对象,这个数字将不断增加。

在旧版本的Windows中,这可能会导致整个应用程序崩溃(据我所记,限制为10000),但对于Vista/7我不确定,但仍然是一件坏事。


GDI+ 使用 GDI 对象吗?据我所知,WinForms 在大多数功能上使用的是 GDI+ 而不是 GDI - CodesInChaos
@CodeInChaos - 快速测试确认,在OnPaint事件中拥有Pen对象会增加GDI对象的值,因此可能包含了GDI和GDI+对象。 - Shadow The Spring Wizard
假设一个程序需要使用许多不同颜色的笔。在以下选项之间,有什么权衡之处:(1)每个控件都产生和保留一组笔,并在其自身被Dispose时Dispose它们;(2)总是按需创建笔,从不缓存它们;(3)拥有一个全局的Dictionary<Color, Pen>,它持续应用程序的时间,或者(4)拥有一个全局的Dictionary<Color, WeakReference>,它将保存对每个笔的WeakReference - supercat
最近我在我的一个项目中遇到了类似的问题,最终采用了第三种方法——创建全局字典,并在应用程序销毁时将其释放。它运行得很好,速度也很快,但不能说这是官方或最佳的方法。 - Shadow The Spring Wizard
@ShadowWizard:这似乎类似于字符串的实习。在控件将一个图形对象保存在字段中的情况下,WeakReference方法也似乎有一定的优点,但是许多控件可能希望保存相同的对象(因此保存特定对象的控件数量可能从数百个到零不等)。 - supercat

-1

垃圾回收器最终会回收它,但是时间很重要: 如果您没有调用不再使用的对象的dispose方法,它将在内存中存活更长时间,并被提升到更高的代,这意味着回收它的成本更高。


-3

在我脑海中首先浮现的想法是,这个对象将在方法执行完毕后立即被处理掉!我不知道我从哪里得到了这个信息!这是正确的吗?


1
在C#中(或者.NET通常也是如此),不会发生那样的事情。 - Bryan

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