尽管可以释放内存,为什么我会得到OutOfMemory Exceptions?

时间:2010-10-23 15:17:16

标签: .net garbage-collection

在处理大量图片时,我偶然发现了OutOfMemory-Exception(顺序,不是并行)。我在代码的一小部分中重现了这种行为:

class ImageHolder
{
    public Image Image;

    ~ImageHolder()
    {
        Image.Dispose();
    }
}

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        for (int i = 0; i < 1000; i++)
        {
            ImageHolder h = new ImageHolder() { Image = new Bitmap(1000, 1000) };
        }
    }
}

内存使用率上升和上升,直到我得到异常(有时是ArgumentException,有时是OutOfMemory Exception)。

我的问题不是我能做些什么(我可以在ImageHolder中实现IDisposable,例如使用using-block)。

我的问题是:为什么垃圾收集不会破坏ImageHolder类型的对象(从不调用析构函数),因为没有对它们的引用而且我的内存不足!

感谢您的解释,

菲利普

1 个答案:

答案 0 :(得分:7)

Bitmap类是一大堆非托管代码的托管类包装器,名为GDI +。包装器本身使用非常少的内存,实际的位图像素(通过非托管代码)存储在非托管内存中。垃圾收集器无法触及该内存,它只能看到包装器。这也是Bitmap具有Dispose()方法的原因,它释放了非托管内存。

你得到的OOM是GDI +告诉包装器它不能再分配非托管内存了。是,或者当GDI +随机决定你传递的宽度或高度太大而不是抛出OOM时ArgumentException。 GDI +因抛出无信息的例外而臭名昭着。

不会调用终结器,因为您的程序首先会对GDI +异常进行轰炸。失败的内存分配是来自垃圾收集堆的内存分配,它是不能再分配的非托管代码。

终结器代码错误,当终结器运行时,位图可能已经自行完成。你必须让ImageHolder实现IDisposable,如下所示:

    class ImageHolder : IDisposable {
        public Image Image;

        public void Dispose() {
            if (Image != null) {
                Image.Dispose();
                Image = null;
            }
        }
    }

现在你已经准备好防止OOM:

        for (int i = 0; i < 1000; i++) {
            using (var h = new ImageHolder() { Image = new Bitmap(1000, 1000) }) { 
                // do something with h
                //...
            }
        }

如果你真的需要存储数千个大图像,那么你需要一台可以提供1000 x 1000 x 1000 x 4 = 4千兆字节虚拟内存的机器。这是可能的,64位操作系统可以为您提供。

让您远离麻烦的一般经验法则是,实现您自己的析构函数非常。 .NET类的工作是为非托管资源提供包装器。像Bitmap一样。那些包装类有一个终结器,你不需要(也不应该)自己提供。 99.99%的情况是您需要实现IDisposable,因此您可以在.NET类实例上调用Dispose()。即使您想要管理自己的操作系统资源,那么仍然不会。您应该使用其中一个SafeHandle包装器。