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

本文关键字:内存 泄漏 机器 | 更新日期: 2023-09-27 17:54:08

背景信息

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

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

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

为了管理内存使用,我创建了一个CompressibleImage类,它封装了位图文件并在FileStream上读取/写入图像文件。当应用程序不再需要图像时,会将其写入文件流。当应用程序需要图像时(即用户双击缩略图(,会从流中创建位图文件。以下是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. 从扫描的位图创建可压缩图像
  3. 将位图写入文件流

save方法在我的机器上运行良好,其行为如下: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) {
                    ...
                }
            }
        }
    }

问题描述

在我的机器中,应用程序运行良好。没有内存泄漏,扫描保存方法可以顺利完成任务,并具有合理的内存使用率(扫描100张纸张,拾取量小于<140MB(。

问题是,当我试图在其他机器上测试应用程序时,垃圾回收器没有释放内存,导致在执行这两个方法期间以及当图像量相当高时(>40(出现MemoryException。异常在CompressibleImage内部引发。当我尝试解压缩(读取(图像时,GetDecompressedImage((方法:

decompressedImage = new Bitmap(stream);

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

在类似的机器上可能会有如此不同的行为?

系统信息

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

  • 处理器:Intel i7 2.30GHz
  • 内存:8GB
  • 类型:64位
  • 操作系统:Windows 7 Pro SP 1

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

不太确定您的MemoryException,请提供完整的堆栈。

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

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

下面的示例显示了实现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.
    }
}

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

MSDN关于"使用"语句的文章