.NET内存问题:加载约40张图像时,内存未被回收,可能是由于LOH碎片化引起的。

36

这是我第一次尝试对 .NET 应用程序进行内存分析(我已经完成了 CPU 调整),但我在这里遇到了一些问题。

我的应用程序中有一个视图,每页加载40张图片,每张图片大约为3MB。最多只有10页。考虑到我不想一次性在内存中保存400张图片或1.2GB的数据,因此在页面更改时将每个图像设置为 null。

起初,我认为可能只是我对这些图像的引用过期了。我下载了 ANTS Profiler(顺便说一句,这是一个很棒的工具),并运行了一些测试。对象生命周期图告诉我,除了父类中的单个引用(这也是按设计实现的,通过仔细检查我的代码得到证实)之外,我没有任何与这些图像相关的引用:

enter image description here

父类 SlideViewModelBase 会一直保留在缓存中,但当页面更改时,MacroImage 属性被设置为 null。我看不到任何这些对象应该比预期保留更长时间的迹象。

我接着查看了大对象堆和内存使用情况。在查看了三页图片后,我已经分配了691.9MB的非托管内存和442.3MB的 LOH 内存。来自我的 System.Drawing.BitmapBitmapImage 转换的 System.Byte[] 几乎占用了所有 LOH 空间。这是我的转换代码:

public static BitmapSource ToBmpSrc( this Bitmap b )
{
    var bi = new BitmapImage();
    var ms = new MemoryStream();
    bi.CacheOption = BitmapCacheOption.OnLoad;
    b.Save( ms,  ImageFormat.Bmp );
    ms.Position = 0;
    bi.BeginInit();
    ms.Seek( 0, SeekOrigin.Begin );
    bi.StreamSource = ms;
    bi.EndInit();
    return bi;
}

我很难找到未经管理的内存去向。一开始我怀疑是System.Drawing.Bitmap对象的问题,但是 ANTS 没有显示它们继续存在,而且我还运行了一个测试,确保全部对象都已被处理掉,但没有任何改变。因此,我仍未弄清楚这些未经管理的内存究竟来自哪里。

我的两个目前的理论是:

  1. LOH 内存碎片问题。如果我离开分页视图并点击几个按钮,约1.5GB的内存会被回收。虽然仍然太多,但还是引人入胜的。
  2. 某种奇怪的 WPF 绑定问题。我们使用数据绑定来显示这些图像,而我对这些 WPF 控件的详细工作原理并不是很熟悉。

如果有任何理论或分析提示,我将非常感激,因为(当然)我们面临着紧迫的最后期限,我正在努力完成这最后一部分工作。我觉得我已经被在 C++ 中追踪内存泄漏给惯坏了... 谁能想到呢?

如果您需要更多信息或者想让我尝试其他事情,请问我。很抱歉这里是一堵文字墙,我尽量让它简洁明了。


如何处理MemoryStream的释放? - Yuriy Faktorovich
很遗憾,那样做行不通。根据文档,BitmapCacheOption.OnLoad 应该会清除流,我已经尝试手动处理它,但没有成功。谢谢你的回答,不过我应该在问题中添加这一点。 - Ed S.
@Yuriy Faktorovich:另一个问题是,即使使用OnLoad选项,如果我手动释放它,图像也不会出现。该对象是有效的,但图像数据已经丢失。 - Ed S.
1
@H.B.:你告诉我...天哪。虽然如此,我喜欢这个地方,它为我节省了多少工作时间。 - Ed S.
@EdS:结合知识的美妙力量 :) - H.B.
显示剩余2条评论
2个回答

39
这篇博客文章似乎描述了你所遇到的问题,而提出的解决方案是创建一个包装另一个流的流实现
这个包装类的Dispose方法需要释放被包装的流,以便它可以被垃圾回收。一旦BitmapImage用这个包装流初始化,包装流就可以被处理掉,释放底层流,从而允许大的字节数组本身被释放。

BitmapImage保留了对源流的引用,因此它使MemoryStream对象保持活动状态。不幸的是,即使已调用MemoryStream.Dispose,它也不会释放内存流所包装的字节数组。因此,在这种情况下,位图正在引用流,流正在引用缓冲区,后者可能占据大对象堆上的很多空间。这并不是真正的内存泄漏;当没有更多的位图引用时,所有这些对象(最终)将被垃圾回收。但由于位图已经制作了自己的图像私有副本(用于渲染),因此在内存中仍然存在不必要的原始位图副本似乎相当浪费。

此外,你使用的是哪个版本的.NET?在.NET 3.5 SP1之前,有一个已知问题,BitmapImage可能会导致内存泄漏。解决方法是对BitmapImage调用Freeze

4
这就是问题所在,我希望我能再给你1000分。我曾经在一个测试中手动处理掉了底层流,认为这不是问题的原因,但我想这就是我的疏忽造成的结果。再次感谢。 - Ed S.
是的!这解决了我一个噩梦般的问题 - 我无法感谢你们足够 :-) - Phil Jenkins
我在.NET 4.6.1中遇到了这个问题。这个解决方案适用吗? - SuperJMN
3
两个链接都失效了。有没有想法在哪里找到那个内容? - Clemens
复制在此处:https://web.archive.org/web/20100823040205/http://code.logos.com/blog/2009/05/wrappingstream_implementation.html - goldcode
显示剩余5条评论

2

你在哪里关闭和处理内存流?如果GC(垃圾回收器)需要将对象移动到更高的代数才能执行析构函数(通常会调用dispose函数),那么它可能需要更加努力地释放资源。

在你的情况下,只有在完成图像操作后才能处理内存流。当你想要卸载它们时,请遍历这些图像并尝试处理内存流。


是的,Yuri刚让我尝试了别的东西。一开始我已经试过释放流,但问题是图片......变成了空白。我以为使用OnLoad选项可以帮我处理掉它根据文档,但我想我误解了它的意思。在清除图像时,我曾经释放过StreamSource,但那时候它没有任何明显的影响。不过我会再深入研究一下,因为这个流才是问题所在。 - Ed S.
是的,我看到了Oppositional在下面描述的内容。 - Ed S.

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