【问题标题】:C# Memory Leak on on different machines不同机器上的 C# 内存泄漏
【发布时间】:2015-08-23 00:32:52
【问题描述】:

背景信息

我使用 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. 从扫描的位图创建 CompressibleImage
  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) {
                    ...
                }
            }
        }
    }

问题描述

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

问题是,当我尝试在其他机器上测试应用程序时,垃圾收集器没有释放内存,导致在这两种方法的执行过程中以及当图像相当高(> 40)。当我尝试解压缩(读取)图像时,CompressibleImage.GetDecompressedImage() 方法中会引发异常:

decompressedImage = new Bitmap(stream);

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

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

系统信息

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

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

【问题讨论】:

  • 不是说这是问题所在,而是你的析构函数实现不对。您不应该在其中引用其他托管对象,因为不能保证它们仍然可以访问。研究实现IDisposable 接口。请参阅here 了解更多信息。

标签: c# memory-leaks bitmap garbage-collection


【解决方案1】:

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

MSDN Article on 'using' statement

【讨论】:

  • 对不起,这个解释很奇怪。我同意 OP 应该使用IDisposable 模式,它可以使用using。但是using 并没有像您暗示的那样触发GC。另外,我假设您的意思是析构函数。但是,关于析构函数,如果实施得当,IDisposable 模式应该会阻止调用destructor,而不是相反。
  • 答案已编辑,你说得对,我确实混淆了一些事实。
【解决方案2】:

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

但是,我可以看到您在析构函数中犯了一个明显的错误。 你不应该在析构函数中引用你的托管资源。原因是 GC 和 Finalizer 使用启发式算法来触发它们,您永远无法预测托管对象的 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.
    }
}

【讨论】:

  • 感谢您的回答。我来自 C++,刚刚开始处理托管/非托管资源。该方案有效地提高了内存使用率。我仍在运行更多测试以确保在所有测试机器上都能正常工作。顺便说一句,我仍然有点困惑,为什么在以前的解决方案中我有如此不同的行为。你有没有遇到过这样的问题?
  • @Nickeat :我猜,这是因为在析构函数中引用了托管对象。可能是发布和调试版本有一些影响..不太确定。
猜你喜欢
  • 2021-01-31
  • 1970-01-01
  • 1970-01-01
  • 2016-01-27
  • 2010-11-11
  • 2017-02-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多