1

我一直在开发一个数据导出程序,该程序从数据库中提取一堆记录。其中一个步骤涉及将 RTF 文本字符串转换为纯文本,这最终导致用户对象在运行时发生内存泄漏。任务管理器将显示的列之一是“USER objects”——当它达到 ~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 被处理掉,但事实并非如此。

4

7 回答 7

6

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 实现对象,都应该通过 via 处理。

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

于 2013-09-11T17:14:22.867 回答
6

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

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

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

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

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

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

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

于 2013-09-11T17:23:16.877 回答
2

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

于 2013-09-11T17:15:26.230 回答
1

.NET 运行时及其垃圾收集器仅在对象是托管对象时才会处理该对象。非托管对象必须由您处置。这是因为非托管对象可以使用不受运行时控制的资源。如果运行时本身脱离了这些对象,那么系统内存中就会有很多其他进程无法处理的垃圾。

您可能想看看这个问题:.NET 中“托管”资源与“非托管”资源是什么意思?

于 2013-09-11T17:15:30.310 回答
0

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

这正是IDisposable它的用途。

于 2013-09-11T17:15:48.617 回答
0

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

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

看看 C# 中的“using”语句,这将使IDisposable对象的使用更容易。

一些进一步的阅读:

IDisposable:http: //msdn.microsoft.com/en-us/library/system.idisposable.aspx

使用声明:http: //msdn.microsoft.com/en-us/library/yh598w02.aspx

于 2013-09-11T17:16:40.347 回答
0

.NET 只知道 .NET 分配的内存。这意味着如果任何代码调用分配内存的“非托管”代码,.NET不知道该内存存在。

这就是 .Dispose() 存在的原因,因此您可以提前处理“非托管”内存。制作使用非托管内存的 .NET 对象的人应该实现 IDisposable。

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

于 2013-09-11T17:18:13.260 回答