【问题标题】:What happens if I don't call Dispose on the pen object?如果我不对笔对象调用 Dispose 会发生什么?
【发布时间】:2010-11-24 14:14:03
【问题描述】:

如果我不在此代码 sn-p 中的 pen 对象上调用 Dispose 会发生什么?

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

【问题讨论】:

  • 没什么。这就是你应该调用它的原因。
  • 在我看来,关于该方法的作用的 MSDN 文档非常清楚。 msdn.microsoft.com/en-us/library/…
  • 好吧,我想我可以提出这个问题,如果我不打电话给Dispose不会会发生什么,但我想你明白我的意思了吗?
  • .net 不做逃逸分析。因此它不知道在pen 超出范围后引用是否仍然存在。因此,您必须等到 GC 决定收集 Pen,这可能要晚得多。
  • 如果你懒得输入myobj.Dispose(),你必须用[using声明](msdn.microsoft.com/en-us/library/yh598w02(v=VS.80).aspx).

标签: c# winforms dispose


【解决方案1】:

这里应该做一些更正:

关于 Phil Devaney 的回答:

“...调用 Dispose 可让您进行确定性清理,强烈推荐。”

实际上,调用 Dispose() 不会确定性地导致 .NET 中的 GC 收集 - 即它不会仅仅因为您调用 Dispose() 而立即触发 GC。它只是间接地向 GC 发出信号,表明可以在下一次 GC 期间清理对象(对于对象所在的 Generation)。换句话说,如果对象存在于 Gen 1 中,那么在 Gen 1 收集发生之前它不会被处置。您可以以编程方式和确定性地使 GC 执行收集的唯一方法之一(尽管不是唯一的)是调用 GC.Collect()。但是,不建议这样做,因为 GC 在运行时通过收集有关应用程序运行时内存分配的指标来“调整”自身。调用 GC.Collect() 会转储这些指标并导致 GC 重新开始其“调整”。

关于答案:

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

这是不完整的。由于 GC 是不确定的,因此可以使用 Dispose 模式 (How to properly implement the Dispose pattern),以便您可以释放正在使用的资源 - 托管或非托管。它与您发布的资源类型无关。实现终结器的需求确实与您使用的资源类型有关 - 即,只有在您拥有不可终结(即本机)资源时才实施一个。也许你混淆了两者。顺便说一句,您应该避免使用 SafeHandle 类来实现终结器,而是使用封装通过 P/Invoke 或 COM 互操作封送的本机资源。如果您最终实现了终结器,则应该始终实现 Dispose 模式。

我还没有看到任何人提到的一个重要注意事项是,如果一次性对象被创建并且它有一个终结器(你永远不知道他们是否这样做 - 你当然不应该对那),然后它将被直接发送到终结队列并至少存在 1 个额外的 GC 集合

如果最终没有调用 GC.SuppressFinalize(),则对象的终结器将在下一次 GC 时调用。请注意,Dispose 模式的正确实现应该调用 GC.SuppressFinalize()。因此,如果您在对象上调用 Dispose(),并且它已经正确实现了模式,您将避免执行终结器。如果您不对具有终结器的对象调用 Dispose(),则该对象的终结器将由 GC 在下一次收集时执行。为什么这很糟糕? CLR 中的终结器线程(包括 .NET 4.6)是单线程的。想象一下,如果您增加此线程的负担会发生什么 - 您的应用性能取决于您知道的位置。

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

  1. 减少过程中 GC 的应变;
  2. 降低应用的内存压力;
  3. 如果 LOH(大对象堆)碎片化并且对象位于 LOH 上,则减少 OutOfMemoryException (OOM) 的机会;
  4. 如果对象具有终结器,则将其排除在可终结和可访问队列之外;
  5. 确保清理您的资源(托管和非托管)。

编辑: 我刚刚注意到IDisposable 上的“无所不知且始终正确”的 MSDN 文档(此处极度讽刺)实际上确实说

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

正如任何人都应该知道的那样,MSDN 远非正确,从未提及或展示“最佳实践”,有时会提供无法编译的示例等。不幸的是,这些话中记录了这一点。但是,我知道他们想说什么:在一个完美的世界里,GC 会为你清理所有托管资源(多么理想化);但是,它不会清理 非托管 资源。这是绝对正确的。话虽如此,生活并不完美,任何应用也不完美。 GC 只会清理没有根引用的资源。这主要是问题所在。

在 .NET 可以“泄漏”(或不释放)内存的大约 15-20 种不同方式中,如果您不调用 Dispose(),最有可能会咬到您的一种方式是无法取消注册/取消挂钩/取消连接/detach 事件处理程序/委托。如果您创建了一个对象,该对象具有连接到它的委托,并且您没有在其上调用 Dispose()(并且不要自己分离委托),则 GC 仍会将对象视为具有根引用 - 即委托。因此,GC 永远不会收集它。

@joren 的评论/问题如下(我的回复太长,无法评论):

我有一篇关于我推荐使用的 Dispose 模式的博文 - (How to properly implement the Dispose pattern)。有时您应该取消引用,这样做永远不会有坏处。实际上,这样做确实在 GC 运行之前做了一些事情——它删除了对该对象的根引用。 GC 稍后扫描它的根引用集合并收集那些没有根引用的引用。最好考虑一下这个例子:你有一个“ClassA”类型的实例——我们称它为“X”。 X 包含一个“ClassB”类型的对象——我们称之为“Y”。 Y 实现了 IDisposable,因此,X 应该做同样的事情来处理 Y。让我们假设 X 在第 2 代或 LOH 中并且 Y 在第 0 代或第 1 代中。当在 X 上调用 Dispose() 并且该实现使对 Y 的引用,对 Y 的根引用会立即被删除。如果第 0 代或第 1 代发生 GC,则 Y 的内存/资源会被清理,但 X 的内存/资源不会因为 X 位于第 2 代或 LOH 中。

【讨论】:

  • 对于明显的矛盾,Jason 表示“IDisposable 是用于处置非托管资源”。它的唯一目的是确定性地清理资源——无论它们是否被管理。正确实施 Dispose 模式可以让您清理资源。我并不是说未清理非托管资源,而是说 IDisposable 的目的与您正在处理的资源类型是正交的。人们可以从他的陈述中推测,除非您使用本机资源,否则您不需要实现 IDisposable - 这绝对是不真实的。
  • 我明白你的意思。但是,如果我执行this.Foo.Dispose(); this.Foo = null;,那么Foo.Dispose 是否将其引用归零都无关紧要,因为无论如何都不再有从GC 根到对象的路径。如果您只使用 Dispose 来为空引用,那么您可能根本不使用 Dispose,而只是直接摆脱对对象的引用。所以 MSDN 关于主要用途是释放非托管资源的说法确实是正确的,因为没有它,只要你不坚持对你不使用的对象的引用,其他一切都可以同样好地实现。
  • @davidcarr - 我认为你完全错过了我回答的重点。避免在托管代码中调用 Dispose 是不行的。我已经详细解释了为什么山雀是必要的。底线是:如果一个类实现了 IDisposable,你应该调用它。要说在一种情况下你不需要调用 Dispose 因为某个类的实现方式是糟糕的设计——你依赖于应该被视为黑匣子的内部实现细节——例如MemoryStream 和任何其他 .NET Framework(或其他外部框架)类。内部实现发生变化。
  • @davidcarr - IDisposable 模式被如此误解的原因之一是人们(比如你自己)想要关心底层资源是使用托管资源还是非托管资源 - 好像这是决定因素- 它应该是决定因素。如果您坚持“如果它实现了 IDisposable,那么我应该调用 Dispose()”的想法,那么您 100% 的时间都会没事的。如果人们听从了这个建议,那么模式上的混乱就会很小(如果有的话)。
  • @davidcarr 作为记录,当我说“像你这样的人”时,我并没有不尊重的意思。我只是描述性的。
【解决方案2】:

Pen 将在未来某个不确定的时间点被 GC 收集,无论您是否调用 Dispose

但是,笔持有的任何非托管资源(例如,GDI+ 句柄)都不会被 GC 清理。 GC 只清理托管资源。调用Pen.Dispose 可以确保及时清理这些非托管资源并且不会泄漏资源。

现在,如果Pen 有一个终结器,并且该终结器清理了非托管资源,那么当Pen 被垃圾回收时,这些非托管资源将被清理。但重点是:

  1. 您应该显式调用Dispose,以便释放您的非托管资源,并且
  2. 您不必担心是否存在终结器并清理非托管资源的实现细节。

Pen 实现IDisposableIDisposable 用于处置非托管资源。这是 .NET 中的模式。

有关此主题的以前的cmet,请参阅answer

【讨论】:

  • 这个答案不完整,有点误导。我已经在下面的帖子中描述了原因。
  • -1 表示 GC 不会清理非托管资源。如果 IDisposable 实现正确地执行此操作,它将在终结线程中正常工作,但只是稍后。
  • GC 不会释放非托管内存或资源——无论 Dispose() 是如何实现的。它可以做的是允许您调用 Marshal.ReleaseComObject() 并实现一个 Finalizer,如果 GC.SuppressFinalize() 没有被调用,它将被调用。仅仅因为非托管内存的清理发生在 Finalization 或 Dispose 实现中,并不意味着 GC 是执行释放的那个。 GC 不负责清理非托管资源——如果程序员从不这样做,那么内存就会泄漏。
  • @DaveBlack Pen 上的 MSDN 文档 (msdn.microsoft.com/en-us/library/…) 不同意你的观点。 “总是在释放对 Pen 的最后一个引用之前调用 Dispose。否则,在垃圾收集器调用 Pen 对象的 Finalize 方法之前,它正在使用的资源不会被释放。”除非写得不好,否则我认为“资源”都是资源,也是非托管资源。所以在这种情况下 GC 确实释放了非托管资源。
  • @Tom - GC 对非托管资源一无所知:不知道如何分配它们,也不知道如何释放它们。该语言的作用是为您提供一种通过带有终结器的 Dispose 自己处理它的方法。从技术上讲, Pen 类不是非托管资源;相反,它是非托管资源的托管包装器。 Pen 类中的底层代码实现了一个终结器。该类中的终结器执行必要的清理任务,因此在处理托管对象时,资源通常不会“泄漏”(很少有例外)。
【解决方案3】:

底层 GDI+ 笔句柄直到未来某个不确定的时间才会被释放,即当 Pen 对象被垃圾收集并调用对象的终结器时。这可能直到进程终止,或者可能更早,但关键是它的不确定性。调用 Dispose 可让您进行确定性清理,强烈推荐。

【讨论】:

  • 这个答案有点不正确。我已经在下面的帖子中描述了原因。
【解决方案4】:

正在使用的 .Net 内存总量是 .Net 部分 + 正在使用的所有“外部”数据。操作系统对象、打开的文件、数据库和网络连接都占用了一些非纯 .Net 对象的资源。

Graphics 使用 Pen 和其他对象,它们实际上是 OS 对象,保存起来“相当”昂贵。 (您可以将 Pen 换成 1000x1000 位图文件)。仅当您调用特定的清理函数后,这些操作系统对象才会从操作系统内存中删除。 Pen 和 Bitmap Dispose 函数会在您调用它们时立即为您执行此操作。

如果您不调用 Dispose,垃圾收集器将在“未来某处*”来清理它们。 (它实际上会调用可能调用 Dispose() 的析构函数/终结代码)

*在未来某处具有无限内存(或超过 1GB)的机器上,未来可能非常遥远。在一台什么都不做的机器上,清理那个巨大的位图或非常小的笔很容易超过 30 分钟。

【讨论】:

    【解决方案5】:

    如果你真的想知道当你不对图形对象调用 Dispose 时有多糟糕,你可以使用 CLR Profiler,可免费下载 here. 在安装文件夹中(默认为 C:\CLRProfiler )是 CLRProfiler.doc,它有一个很好的例子,说明当您不对 Brush 对象调用 Dispose 时会发生什么。这是非常有启发性的。简短的版本是图形对象占用的内存比您预期的要大,并且除非您对它们调用 Dispose,否则它们可以挂起很长时间。一旦对象不再使用,系统最终会清理它们,但该过程占用的 CPU 时间比您在完成对象时调用 Dispose 时要多。 您可能还想了解如何使用 IDisposable herehere

    【讨论】:

      【解决方案6】:

      它会保留资源直到垃圾收集器清理它

      【讨论】:

      • 黄!句柄将一直保留到进程终止。
      • pen 终结器在运行时应该清理资源msdn.microsoft.com/en-us/library/… 当然不能保证它会运行
      • @Mitch、@Aliostad、@jk - 所有扩展 Component 的对象都实现了一个调用 Dispose 的终结器。终结器是非确定性的,这意味着您无法预测它何时运行,但它最终总会运行。如果另一个终结器无限期阻塞或进程被终止,那么它当然不会运行。
      • @ChaosPandion:在未捕获的异常后卸载 AppDomain 时,不会调用普通的终结器。为此,您需要一个关键的终结器。但我猜 Pen 使用了安全句柄,因此使用了关键的最终确定,但我没有验证这一点。
      【解决方案7】:

      取决于它是否实现终结器并在其终结方法上调用 Dispose。如果是这样,句柄将在 GC 时释放。

      如果没有,句柄将一直存在,直到进程终止。

      【讨论】:

      • "在释放对 Pen 的最后一个引用之前始终调用 Dispose。否则,在垃圾收集器调用 Pen 对象的 Finalize 方法之前,不会释放它正在使用的资源。" msdn.microsoft.com/en-us/library/…
      【解决方案8】:

      使用图形内容可能会非常糟糕。

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

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

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

      【讨论】:

      • GDI+ 是否使用 GDI 对象? Afaik WinForms 的大部分功能使用 GDI+ 而不是 GDI。
      • @CodeInChaos - 快速测试确认在 OnPaint 事件中使用 Pen 对象会提高 GDI 对象的值,因此它可能同时包含 GDI 和 GDI+ 对象。
      • 假设一个程序需要使用许多不同颜色的笔。 (1)有多个控件每个产生并保留一组笔之间的权衡是什么,Disposeing 当它本身是Disposed; (2) 始终按需创建​​画笔并且从不缓存它们; (3) 拥有一个与应用程序一样长的全局 Dictionary<Color, Pen>,或者 (4) 拥有一个全局 Dictionary<Color, WeakReference>,它将为每支笔保存一个 WeakReference
      • @supercat 最近我在我的一个项目中遇到了类似的问题,最终得到#3 - 全局字典创建一次并在应用程序被销毁时处理。它运行良好且快速,但不能说这是官方或最佳方式。
      • @ShadowWizard:这似乎类似于实习字符串。 WeakReference 方法似乎在控件将在字段中保存图形对象的情况下也有一些优点,但许多控件可能希望保存相同的对象(因此保存特定对象的控件数量可能从数百个不等为零)。
      【解决方案9】:

      垃圾收集器无论如何都会收集它,但在以下情况下很重要: 如果您不对不使用的对象调用 dispose,它将在内存中存活更长时间并被提升到更高代,这意味着收集它的成本更高。

      【讨论】:

        【解决方案10】:

        在我的脑海中浮现的第一个想法是,一旦方法执行完毕,这个对象就会被释放!我不知道我从哪里得到这个信息!对吗?

        【讨论】:

        • 在 C#(或一般的 .NET)中不会发生这种情况
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-04-02
        • 1970-01-01
        • 2014-10-28
        • 2013-04-01
        • 2012-01-13
        • 2011-03-15
        相关资源
        最近更新 更多