垃圾回收无法回收BitmapImage?

7
我有一个创建大量BitmapImages(例如25000)的应用程序(WPF)。似乎框架使用一些内部逻辑,因此在创建后会消耗约300 MB的内存(150虚拟和150物理)。这些BitmapImages添加到Image对象中,然后添加到Canvas中。问题是当我释放所有这些图像时,内存并没有被释放。我该如何释放内存?
这个应用程序很简单: Xaml
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Canvas x:Name="canvas" Grid.ColumnSpan="2"></Canvas>
        <Button Content="Add" Grid.Row="1" Click="Button_Click"/>
        <Button Content="Remove" Grid.Row="1" Grid.Column="1" Click="Remove_click"/>
    </Grid>

代码后台

        const int size = 25000;
        BitmapImage[] bimages = new BitmapImage[size];
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var paths = Directory.GetFiles(@"C:\Images", "*.jpg");
            for (int i = 0; i < size; i++)
            {
                bimages[i] = new BitmapImage(new Uri(paths[i % paths.Length]));
                var image = new Image();
                image.Source = bimages[i];
                canvas.Children.Add(image);
                Canvas.SetLeft(image, i*10);
                Canvas.SetTop(image, i * 10);
            }
        }

        private void Remove_click(object sender, RoutedEventArgs e)
        {
            for (int i = 0; i < size; i++)
            {
                bimages[i] = null;
            }
            canvas.Children.Clear();
            bimages = null;
            GC.Collect();
            GC.Collect();
            GC.Collect();
        }

这是在添加图片后的ResourceManager的屏幕截图:enter image description here

1
消耗了约300MB的内存(150虚拟和150物理),这完全是错误的。请仔细阅读有关内存的资料。 - leppie
2
不要使用GC.Collect(),而是使用Bitmap.Dispose()。 - Steve B
你在那里调用 GC 是没有任何作用的。假设 BitmapImage 对象没有未解除引用的根引用,当 CLR 需要时,它将回收内存。 - MoonKnight
2
@Steve B WPF的BitmapImage没有Dispose方法。 - MoonKnight
1
抱歉,我对System.Drawing.Bitmap类感到困惑。 - Steve B
显示剩余2条评论
5个回答

6
在Wpf中存在一个bug,我们曾被其困扰过。如果不将BitmapImage对象冻结,它们就不会被释放。我们在https://www.jawahar.tech/home/finding-memory-leaks-in-wpf-based-applications上发现了这个问题。虽然该问题应该已经在Wpf 3.5 sp1中得到解决,但在某些情况下我们仍能看到它。尝试按以下方式更改您的代码以查看是否存在此问题:
bimages[i] = new BitmapImage(new Uri(paths[i % paths.Length]));
bimages[i].Freeze();

我们现在经常会冻结我们的BitmapImage对象,因为我们在分析器中看到了其他实例,Wpf正在监听BitmapImage上的事件,从而使图像保持活动状态。如果调用Freeze()不是您代码的明显修复方法,我强烈建议使用诸如RedGate Memory Profiler之类的分析器 - 它将跟踪依赖关系树,以显示是什么使得您的Image对象保持在内存中。

已编辑上面的内容,包括原始URL和Profiler建议,如果Freeze()不是明显的修复方法。 - fubaar
1
请问在你的情况下,Freeze()调用是否解决了问题? - fubaar
+1 感谢链接更新。在我看来,这很可能是最接近的故障,并值得调查。 - Adam Houldsworth
1
@fubaar 不,应用程序在调用BitmapImages的冻结上并没有受益。 - Blablablaster
@fubaar,链接现在已经失效。 - Graviton
@Graviton 更新了帖子,并附上了博客作者新页面的链接。 - fubaar

2
我的做法是:
  1. 将Image控件的ImageSource设置为null
  2. 在从UI中移除包含Image的控件之前运行UpdateLayout()。
  3. 确保在创建BitmapImage时冻结它,并且没有对用作ImageSources的BitmapImage对象进行非弱引用。
每个Image的清理方法最终变得如此简单:
img.Source = null;
UpdateLayout();

通过实验,我发现可以通过保留一个列表并在其中添加指向每个创建的BitmapImage的WeakReference()对象,然后在它们应该被清除之后检查WeakReferences上的IsAlive字段来确认它们是否已经被清除。

因此,我的BitmapImage创建方法如下:

var bi = new BitmapImage();
using (var fs = new FileStream(pic, FileMode.Open))
{
    bi.BeginInit();
    bi.CacheOption = BitmapCacheOption.OnLoad;
    bi.StreamSource = fs;
    bi.EndInit();
}
bi.Freeze();
weakreflist.Add(new WeakReference(bi));
return bi;

2

我跟随AAAA的答案。导致内存占用过多的原始代码如下:

if (overlay != null) overlay.Dispose();
overlay = new Bitmap(backDrop);
Graphics g = Graphics.FromImage(overlay);

插入AAAA的代码块,C#中添加“using System.Threading;”,VB中添加“Imports System.Threading”:

if (overlay != null) overlay.Dispose();
//--------------------------------------------- code given by AAAA
Thread t = new Thread(new ThreadStart(delegate
{
    Thread.Sleep(500);
    GC.Collect();
}));
t.Start();
//-------------------------------------------- \code given by AAAA
overlay = new Bitmap(backDrop);
Graphics g = Graphics.FromImage(overlay);

现在重复循环该块可以保持稳定且低内存占用。此代码使用Visual Studio 2015 Community可行。


1
这个答案是针对WPF还是Winforms的?我认为WPF应用程序通常不使用(GDI) GraphicsBitmap 类。 - jrh

1

这仍然是一个数组

BitmapImage[] bimages = new BitmapImage[size];

数组是连续的固定长度数据结构,一旦为整个数组分配了内存,就无法回收其中的部分。尝试使用其他数据结构(如LinkedList<T>)或其他更适合您情况的数据结构。


4
该数组的大小为size乘以指针大小,但是这并没有考虑指向的对象所占用的堆空间。根据上面的示例,这些对象都已被删除和取消引用。因此,尽管该语句准确无误,但它无法解释为什么在垃圾回收后内存使用量不会改变。 - Adam Houldsworth
@AdamHouldsworth 数组由变量bimages引用,存储在本地变量堆栈中。当方法退出时,此局部变量将超出范围,这意味着没有任何东西可以引用内存堆上的数组。然后,孤立的数组就有资格被GC回收。但是,此集合可能不会立即发生,因为CLR决定是否进行收集取决于许多因素(可用内存,当前内存分配等)。这意味着在垃圾回收之前需要花费不确定的时间。 - MoonKnight
以上包括显式的“手动”调用GC。在这种情况下调用GC永远不会保证立即回收... - MoonKnight
1
@Killercam 显式调用将始终捕获指定代的可用项(如果未指定,则为所有项)。如果变量不再使用或手动设置为“null”,CLR实际上甚至可以在方法退出之前使本地变量成为可用,但这是一种优化,不应依赖。 - Adam Houldsworth
1
@Killercam 是的,我对这个问题的最佳猜测是,尽管所有项都看起来已完全取消引用,但仍然存在一些隐秘的问题。也许某个项在WPF引擎的某个地方保留了一个引用。 - Adam Houldsworth
显示剩余2条评论

1

我只是在讲述我关于回收BitmapImage内存的经验。我使用.Net Framework 4.5。
我创建了一个简单的WPF应用程序并加载了一个大型图像文件。我尝试使用以下代码从内存中清除图像:

private void ButtonImageRemove_Click(object sender, RoutedEventArgs e)
    {

        image1.Source = null;
        GC.Collect();
    }

但是它并没有起作用。我尝试了其他解决方案,但是没有得到答案。经过几天的努力,我发现如果我按两次按钮,GC会释放内存。然后我只需编写这段代码,在单击按钮后几秒钟调用GC收集器即可。
private void ButtonImageRemove_Click(object sender, RoutedEventArgs e)
    {

        image1.Source = null;
        System.Threading.Thread thread = new System.Threading.Thread(new System.Threading.ThreadStart(delegate
        {
            System.Threading.Thread.Sleep(500);
            GC.Collect();
        }));
        thread.Start();

    }

这段代码只在DotNetFr 4.5中进行了测试。如果您使用较低版本的.Net Framework,则可能需要冻结BitmapImage对象。
编辑
除非布局得到更新,否则此代码无法工作。我的意思是,如果父控件被移除,GC无法回收它。

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