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

Phi*_*ipp 3 .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) };
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

谢谢你的解释,

菲利普

Han*_*ant 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;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

现在你有机会防止OOM:

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

如果你真的需要存储这些大型图片千那么你就需要一台能够提供1000×1000×1000×4 = 4 GB的虚拟内存.这是可能的,64位操作系统可以为您提供.

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