【问题标题】:Why is there a memory leak when I do not .Dispose() Bitmaps that I .Save() to a MemoryStream?当我不 .Dispose() 将我 .Save() 的位图保存到 MemoryStream 时,为什么会出现内存泄漏?
【发布时间】:2012-02-08 11:42:15
【问题描述】:

假设我创建了一个位图

Bitmap bitmap = new Bitmap(320, 200);

当我将它写入某个流时(在我的例子中,它是一个 HttpResponseStream,由 HttpListenerResponse 给出),一切都很好:

bitmap.Save(stream, ImageFormat.Png);

我不需要bitmap.Dispose(),位图使用的资源会被自动清理。然而,直接将 Png 写入不可搜索的流的问题在于,它可能会导致 A generic error occurred in GDI+,当我在 Azure 上尝试我的 Asp 应用程序时发生了这种情况。这就是我的代码现在的样子:

using (MemoryStream ms = new MemoryStream())
{
  bitmap.Save(ms, ImageFormat.Png);
  ms.WriteTo(stream);
}

现在除非我在 bitmap.Dispose() 之后,这会泄漏。

改写问题以获得更具体的答案: 为什么 Bitmap 内存泄漏似乎取决于我将其保存到哪种类型的流?

更新: 正如我在 cmets 中被问到的那样,我是否确定这是泄漏。在压力测试中反复调用上述方法,我的 w3wp 进程将使用 gigs 和 gigs 的内存,直到我的机器开始交换并且它不会清理。

【问题讨论】:

  • 你确定这是内存泄漏,而不仅仅是垃圾收集还没有清理它吗?
  • 你确定你有内存泄漏吗?他们通过 .Net 的发明重新定义了内存泄漏的定义;)
  • @James;使用 GDI+ 可能会出现系统内存泄漏,一旦 AppDomain 被释放就会被清除。

标签: c# .net bitmap memorystream


【解决方案1】:

位图类使用非托管资源。这些资源与内存流类使用的资源无关。您可以将位图类包装在 using 语句中,以便在完成位图实例后处理它。

错过了你问题的后半部分。 “设置它并忘记它”的一种方法是创建一个包装类,该类公开位图实例但实现一个析构函数来处理位图实例。这个析构函数意味着位图类在垃圾回收时被隐式处理掉。

最后一点:您实例化的任何实现 IDisposable 的对象都必须由您的代码处理掉。 Dipose 永远不会被隐式调用。即使在你的第一个例子中。仅仅因为您将数据保存到流中并不意味着内存已被释放。大多数时候,在实例化它的同一段代码中处理一个对象是个好主意。这有助于通过提高代码透明度来更轻松地阅读代码。

【讨论】:

  • 在没有我调用 bitmap.Dispose() 的情况下,Bitmap 得到或不被清理,仅取决于 Stream I .Save() 它的(类型)。这就是让我感到困惑的地方。顺便说一句, Bitmap.Dispose() 实际上确实被隐式调用, ~Image() 析构函数做到了。检查源代码。
【解决方案2】:

我认为问题在于假设 GC 会神奇地清理您的对象。但是,它可能永远不会这样做,这就是我认为可能发生的事情:

位图使用非托管资源来保存位图数据,位图数据很大。因此,您将为每个位图分配一个tiny 托管内存块和一大块非托管内存。

因此,您将位图放在一边,让 GC 闲暇时收集。这对很多对象都很有效,因为很快就会有足够的内存压力让 GC 收集它们以重新使用内存。但是 GC 查看托管堆并说“通过处理未使用的对象,我只能恢复 64 字节的内存。我不会打扰”。它看不到千兆字节的非托管资源,只有堆上的几个字节。

因此您需要自己跟踪和处理位图。

有时你可能已经看到它为你清理了。这是因为在某些情况下(例如,当您处理其他对象(例如内存占用较大的流)时,或者仅仅因为是星期二下午),它确实选择处理未使用的内存块,并且然后你的位图最后被处理了。但是你不能依靠这件事发生。

...漫步:

在过去,指针存在两个问题。

  • 它们可能为 null,导致代码崩溃
  • 您可能会忘记释放他们的内存/资源并导致泄漏

所以在 .net 中,他们将“指针”重命名为“引用”,添加了 GC 并假装问题不再存在。除了引用仍然可以为空,程序员仍然必须跟踪和管理他们的资源以避免泄漏——只是少了一点。我认为这是一件坏事——它让我们变得懒惰和效率低下,而没有真正消除潜在的问题,所以它又回来咬我们,我们最终编写了大量的 Dispose 逻辑,而我们过去只有一个简单的“删除”在我们的析构函数中。

【讨论】:

    【解决方案3】:

    您必须释放位图才能释放 GDI+ 资源。就是这么简单。这是需要调用 Dispose 的少数几次之一。如果您正在缓存位图以减少磁盘访问,则克隆图像并使用克隆保存到流中。我强烈建议冲洗、关闭和处置流。完成后,将克隆和流变量设置为 null。

    【讨论】:

    • 为什么当我保存到 MemoryStream 以外的东西时没有泄漏?我没有在那里调用 Dispose,它工作正常。添加我在另一条评论中所说的内容:Bitmap.Dispose() 实际上确实被隐式调用, ~Image() 析构函数做到了。
    • 克隆能给我带来什么好处?我不知道克隆有什么作用,但听起来它会消耗更多的内存。 (MSDN 文档仅声明:创建此图像的精确副本。)。顺便说一句,我的情况不涉及磁盘。这些是生成的图像,缓存以权衡 CPU 时间和内存。
    • @EugeneBeresovksy;是的,我知道 Dispose 被隐式调用。我也知道 .NET 会正确初始化和终止 GDI 库。我告诉你的是基于十年的.NET经验,你需要调用Dispose,你需要将保存Image的变量设置为null。
    • @EugeneBeresovksy;克隆可确保您的缓存图像安全并避免潜在的 GDI/GDI+ 错误。
    • @EugeneBeresovksy;如果您不关心内存,那么克隆不是问题。而且,如果你听从我的建议,内存消耗将是微不足道的。
    猜你喜欢
    • 1970-01-01
    • 2013-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-30
    • 2011-09-19
    • 1970-01-01
    相关资源
    最近更新 更多