不同机器上的 C# 内存泄漏

C# Memory Leak on on different machines

背景资料

我开发了一个带有 Windows 表单 (C#) 的桌面应用程序,用于扫描、预览和保存图像。 扫描时的应用程序行为如下:

  1. 扫描n张图片
  2. 为每个图像获取位图并将其存储在临时文件中
  3. 显示调整大小的缩略图作为预览

图像内存管理:可压缩图像

为了管理内存使用,我创建了一个 CompressibleImage class,它在 FileStream 上封装了一个位图文件和 reads/writes 个图像文件。当应用程序不再需要图像时,会将其写入文件流。当应用程序需要图像(即用户双击缩略图)时,会从流中创建一个位图文件。以下是主要的 CompressibleImage 的 方法:

/// Gets the uncompressed image. If the image is compressed, it will be uncompressed
public Image GetDecompressedImage()
    {
        if (decompressedImage == null)
        {
            // Read Bitmap from file stream
            stream.Seek(0, SeekOrigin.Begin);
            decompressedImage = new Bitmap(stream);
        }
        return decompressedImage;
    }


/// Clears the uncompressed image, leaving the compressed one in memory.
public void ClearDecompressedImage()
{
    // If Bitmap file exists, write it to file and dispose it
    if (decompressedImage != null)
    {
        if (stream == null)
        {
            stream = new FileStream(FileStreamPath, FileMode.Create);    
        }
        decompressedImage.Save(stream, format);
        // The Dispose() call does not solve the issue
        // decompressedImage.Dispose();
        decompressedImage = null;
        }
    }

    /// <summary>
    /// Class destructor. It disposes the decompressed image (if this exists), 
    /// closes the stream and delete the temporary file associated.
    /// </summary>
    ~CompressibleImage()
    {
        if (decompressedImage != null)
        {
            decompressedImage.Dispose();
        }
        if(stream != null)
        {
            stream.Close();
            File.Delete(stream.Name);
            stream.Dispose();
        }
    }

应用级别

应用程序主要在扫描方法和保存过程中使用CompressibleImage创建图像文件。 扫描方法工作正常,基本上:

  1. 从扫描仪获取位图
  2. 从扫描的位图创建 CompressibleImage
  3. 将位图写入文件流

保存方法在我的机器上工作正常,其行为如下: 1. 对于每个 CompressibleImage 从流中解压缩(读取和构建)位图 2.保存图像 3.压缩图片

这是保存方法:

private void saveImage_button_Click(object sender, EventArgs e)
    {
        if (Directory.Exists(OutputPath) ==  false && File.Exists(OutputPath) == false)
        {
            Directory.CreateDirectory(OutputPath);
        }

        ListView.CheckedListViewItemCollection checkedItems = listView1.CheckedItems;
        if(checkedItems.Count > 0)
        {
            for (int i = 0; i < checkedItems.Count; ++i)
            {
                int index = checkedItems[i].Index;
                Bitmap image = (Bitmap)compressibleImageList.ElementAt(index).GetDecompressedImage();
                try
                {
                    image.Save(OutputPath + index.ToString() +
                               Module.PNG_FORMAT, ImageFormat.Png);
                    compressibleImageList.ElementAt(index).ClearDecompressedImage();
                    progressForm.Increment();
                    image = null;
                }
                catch (Exception ex) {
                    ...
                }
            }
        }
    }

问题描述

在我的机器上,应用程序运行良好。没有内存泄漏,scansave 方法可以顺利完成工作并且内存使用合理(扫描 100 张小于 < 140MB 的纸张选择)。

问题是,当我尝试在其他机器上测试应用程序时,垃圾收集器没有释放内存,导致在两种方法执行期间出现 MemoryException并且当图像数量相当高(> 40)时。当我尝试解压缩(读取)图像时,CompressibleImage.GetDecompressedImage() 方法中抛出了异常:

decompressedImage = new Bitmap(stream);

虽然我知道 GC 会随机清理内存,但在这种情况下它似乎甚至没有 运行 实际上只有在我关闭应用程序时才会释放内存。

在类似的机器上可能会有这种不同的行为吗?

系统信息

以下是有关测试环境的一些信息。两台机器都有:

当使用包含 IDisposable 接口的 class 打开文件或流时,通常应该使用 using。这将确保在 using 语句之后调用 Dispose 方法。如果正确实施,这将确保释放非托管资源。

MSDN Article on 'using' statement

不太确定你的 MemoryException,请提供完整的堆栈跟踪。

但是,我可以看出您在析构函数中犯了一个明显的错误。 您不应该在析构函数中引用您的托管资源。原因是,GC 和 Finalizer 使用启发式算法来触发它们,您永远无法预测托管对象的终结器的执行顺序。

这就是为什么你应该在你的 dispose 方法中使用 'disposing' 标志并避免在执行来自终结器时接触托管对象。

以下示例展示了实现 IDisposable 接口的一般最佳做法。参考:https://msdn.microsoft.com/en-us/library/system.idisposable.dispose(v=vs.110).aspx

public class DisposeExample
{
    // A base class that implements IDisposable. 
    // By implementing IDisposable, you are announcing that 
    // instances of this type allocate scarce resources. 
    public class MyResource: IDisposable
    {
        // Pointer to an external unmanaged resource. 
        private IntPtr handle;
        // Other managed resource this class uses. 
        private Component component = new Component();
        // Track whether Dispose has been called. 
        private bool disposed = false;

        // The class constructor. 
        public MyResource(IntPtr handle)
        {
            this.handle = handle;
        }

        // Implement IDisposable. 
        // Do not make this method virtual. 
        // A derived class should not be able to override this method. 
        public void Dispose()
        {
            Dispose(true);
            // This object will be cleaned up by the Dispose method. 
            // Therefore, you should call GC.SupressFinalize to 
            // take this object off the finalization queue 
            // and prevent finalization code for this object 
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        // Dispose(bool disposing) executes in two distinct scenarios. 
        // If disposing equals true, the method has been called directly 
        // or indirectly by a user's code. Managed and unmanaged resources 
        // can be disposed. 
        // If disposing equals false, the method has been called by the 
        // runtime from inside the finalizer and you should not reference 
        // other objects. Only unmanaged resources can be disposed. 
        protected virtual void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called. 
            if(!this.disposed)
            {
                // If disposing equals true, dispose all managed 
                // and unmanaged resources. 
                if(disposing)
                {
                    // Dispose managed resources.
                    component.Dispose();
                }

                // Call the appropriate methods to clean up 
                // unmanaged resources here. 
                // If disposing is false, 
                // only the following code is executed.
                CloseHandle(handle);
                handle = IntPtr.Zero;

                // Note disposing has been done.
                disposed = true;

            }
        }

        // Use interop to call the method necessary 
        // to clean up the unmanaged resource.
        [System.Runtime.InteropServices.DllImport("Kernel32")]
        private extern static Boolean CloseHandle(IntPtr handle);

        // Use C# destructor syntax for finalization code. 
        // This destructor will run only if the Dispose method 
        // does not get called. 
        // It gives your base class the opportunity to finalize. 
        // Do not provide destructors in types derived from this class.
        ~MyResource()
        {
            // Do not re-create Dispose clean-up code here. 
            // Calling Dispose(false) is optimal in terms of 
            // readability and maintainability.
            Dispose(false);
        }
    }
    public static void Main()
    {
        // Insert code here to create 
        // and use the MyResource object.
    }
}