为什么我的 C# 方法对用户对象有内存泄漏

本文关键字:对象 内存 泄漏 用户 我的 方法 为什么 | 更新日期: 2023-09-27 18:31:01

我一直在研究一个数据导出程序,该程序从数据库中提取一堆记录。 其中一个步骤涉及将 RTF 文本字符串转换为纯文本,这最终导致用户对象在运行时内存泄漏。 任务管理器将显示的列之一是"USER 对象" - 当达到 ~10,000 时,程序将耗尽分配空间,程序出现"创建窗口句柄时出错"的错误

发生这种情况是因为我没有在方法结束时释放我的对象。

我的问题是,为什么 C#/.net 不为我处理它?

下面是将重现泄漏的代码的快速示例。 将代码放入 Winforms 应用程序中,然后按下按钮使其遍历内存浪费程序。

private void wasteMemory()
{
    System.Windows.Forms.RichTextBox rtfBox = new System.Windows.Forms.RichTextBox();
    //RTF text that reads "Hello World"
    rtfBox.Rtf = "{''rtf1''ansi''ansicpg1252''deff0''deflang1033{''fonttbl{''f0''fnil''fcharset0 Arial;}}  {''colortbl ;''red0''green0''blue0;}  ''viewkind4''uc1''pard''cf1''fs29 Hello World} ";
    //If line below is commented out, User Objects grow out of control.
    //rtfBox.Dispose();
}
private void button1_Click(object sender, EventArgs e)
{
    for (int i = 1; i < 100000; i++)
    {            
        wasteMemory();
    }
}

我的理解是,当方法完成时,在其侧面创建的任何对象都会被释放。 我预计 rtfBox 会被处理掉,但它没有。

为什么我的 C# 方法对用户对象有内存泄漏

到目前为止,这里的每个答案都是不完整的。 是的,确实必须清理非托管资源,但是实现 IDisposable 的类已经这样做了。 这不是重点。

在正确实现 IDisposable 的类中,如果对象未显式或隐式释放,则将在垃圾回收的终结器阶段释放该对象。 但是,当对象超出范围时,此过程不会立即发生。 gc 可能需要几分钟甚至几小时才能运行。

这里的问题是,如果您自己不调用 Dispose()(或通过将其包装在 using 语句中隐式调用 Dispose(),那么(如果类正确实现)对象将不会在垃圾回收器运行之前被释放,这可能需要相当长的时间。

这意味着在垃圾回收器开始处置未引用的对象之前,您可能会用完非托管资源。 而这正是您遇到的问题。

自己调用 Dispose() 可确保非托管对象在完成处理后立即清理它们,而不是在 GC 处理它时清理它们。

把它想象成一个图书馆。 有人借出一本书,书架上有5本。 当其他人借出这个库时,有些人会归还它们。但它们不会立即被放在货架上,它们坐在回邮箱中,直到有人来检查它们并重新上架。

调用Dispose就像将书交给图书管理员,让他们立即将其签入,然后将其放回书架上,以便下一个人可以拿到它。

Dispose 方法是 .NET 方法,它为具有本机资源的对象提供了清理的机会。它有点像C++中的析构函数/删除 - 尽管不是真的。如果不在实现 IDisposable 的对象上调用 Dispose 处理,则这是一个错误,并且很可能导致内存泄漏。最好执行以下操作:

using(System.Windows.Forms.RichTextBox rtfBox = new System.Windows.Forms.RichTextBox())
{
  //RTF text that reads "Hello World"
  rtfBox.Rtf = "{''rtf1''ansi''ansicpg1252''deff0''deflang1033{''fonttbl{''f0''fnil''fcharset0 Arial;}}  {''colortbl ;''red0''green0''blue0;}  ''viewkind4''uc1''pard''cf1''fs29 Hello World} ";
}

using 块的行为将完全符合您的预期。您可以将其视为堆栈上对象C++的方法作用域。

我的理解是,当方法完成时,在其侧面创建的任何对象都会被释放。我预计 rtfBox 会被处理掉,但它没有。

不,这根本不是真的 - 或者大多数其他垃圾收集语言。如果你认识到这里的对象是动态分配的(即非常像指针),那么对于像C++这样的语言来说,它甚至都不是真的,因为当它的指针超出范围时,动态分配的内存不会被清理:你必须显式调用删除。在 .NET 中,对象将被最终确定,析构函数将被调用,当垃圾回收器接近它时,将调用 dispose,但在此之前不会。超出范围只会向垃圾回收器发出信号,表明相关对象有资格被收集。但是,任何具有资源的东西,例如本机代码、文件句柄或其他 IDisposable 实现对象,都应该通过以下方式进行处理。Dispose() 一旦用户完成它们以避免内存泄漏。

有关详细信息,请参阅 http://msdn.microsoft.com/en-us/library/system.idisposable.aspx

.

NET 仅提供内存的自动垃圾回收。它对手柄一无所知,所以你必须自己清理它们。这就是 IDisposable 模式和终结器的用途。

.NET 运行时及其垃圾回收器将仅释放托管对象。非托管对象必须由您处置。这是因为非托管对象可以利用不受运行时控制的资源。如果运行时本身关闭了这些对象,则系统内存中将有大量其他进程无法处理的垃圾。

你可能想要查看此问题:.NET 中的"托管"资源与"非托管"资源是什么意思?

垃圾回收器仅响应内存压力。 如果您有其他受约束的资源,例如 GUI 句柄,则需要确保正确处理它们。 垃圾收集器不声称为您处理这个问题。

这正是IDisposable的目的。

垃圾回收和在代码中调用的 dispose 方法之间存在差异。垃圾回收器将收集不再具有任何引用的对象,但该集合位于 .NET 运行时的域中。如果有任何本机资源与对象关联,则垃圾回收器将无法清除这些资源。

IDisposable接口旨在解决此问题。IDisposable对象的用户应手动调用 Dispose 方法,以使对象有机会自行清理(例如,它可能使用的任何本机资源)。

看看 C# 中的 "using" 语句,它将使IDisposable对象的使用更容易。

一些进一步的阅读:

可识别:http://msdn.microsoft.com/en-us/library/system.idisposable.aspx

使用语句:http://msdn.microsoft.com/en-us/library/yh598w02.aspx

.NET

只知道 .NET 分配的内存。这意味着,如果任何代码调用分配内存的"非托管"代码,则 .NET 不知道此内存是否存在。

这就是为什么.Dispose() 存在,因此您可以提前处理"非托管"内存。创建使用非托管内存的 .NET 对象的人应实现 IDisposable。

如果非托管对象正确实现,那么它还将包含一个"终结器"~ClassName(),每当 GC 进行大整理时,"应该"调用它。但是,您永远不应该依赖正在调用的终结器。如果有的话,终结者只是在那里清理一个进程,如果它突然关闭,或者作为不了解处置模式的开发人员的拐杖。