【问题标题】:Why does my C# method have a memory leak for user objects?为什么我的 C# 方法对用户对象有内存泄漏?
【发布时间】:2013-09-12 01:39:37
【问题描述】:

我一直在开发一个数据导出程序,该程序从数据库中提取大量记录。其中一个步骤涉及将 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 会被处理掉,但事实并非如此。

【问题讨论】:

  • 我怀疑是因为 clr 没有感觉到任何内存压力,所以还没有运行 GC,但是 windows 用完了句柄
  • 代码完整吗?您是否将 rtfBox 附加到其他控件?
  • Windows 句柄是非托管资源(与内存不同),因此 GC 不会“感受到”来自它的压力。在这种情况下,程序员有责任调用 dispose()。

标签: c# .net


【解决方案1】:

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

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

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

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

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

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

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

【讨论】:

  • 这很有意义,谢谢。在原始应用程序中,我在一个非常紧凑的 For 循环中调用了每秒数千次使用 rtfBox 的方法,所以我可以看到为什么这里没有发生 GC。它让我措手不及,因为我认为“感觉它是 C# / .net,我不需要担心内存管理。”如果我不手动执行,那么看看 GC 需要多长时间才能处理它会很有趣。
  • 您假设每个实现 IDisposable 的类也实现了一个终结器。不幸的是,这不是真的(看看流)。 IE。作为 GC 的副作用,清理句柄等资源不一定会自动完成。
  • @BrianRasmussen - 我不确定你对流的意思,因为它们有终结器,并且它们会清理它们的流,除非你明确告诉他们不要这样做。在这种情况下,您知道自己在做什么。
  • @BrianRasmussen - StreamWriter 不是 Stream,这是一个不同的问题。 StreamWriter 没有非托管资源。 Stream 有,而且 Stream 有一个终结器。
  • @BrianRasmussen - 我会说是的,你应该能够依赖这样一个事实,即任何处理非托管资源的类都会有一个终结器,如果没有,那就是一个错误,并且属于我的“正确实施”限定词。 StreamWriter 没有非托管资源,因此在这方面不是问题。
【解决方案2】:

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 处理。用户完成后立即 Dispose() 以避免内存泄漏。

欲了解更多信息,请参阅http://msdn.microsoft.com/en-us/library/system.idisposable.aspx

【讨论】:

  • @gdoron 并且他明确表示他没有使用它,并且当他不使用它时他会看到问题。正确答案是“你应该使用它,如果你没有使用它,那是你的程序中的一个错误,你应该预料到会发生这样的坏事”
  • 所以基本上任何比原语更复杂的东西我应该小心处理?很高兴知道这一点。
  • @EtanSivad,没有任何实现 IDisposable 的东西。
【解决方案3】:

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

【讨论】:

  • 虽然这是真的,但他自己并没有实现 IDisposable。他正在使用的类已经实现了它。所以你的回答或多或少毫无意义,因为他已经知道了。他只是不明白为什么它在超出范围时没有被自动处理,以及为什么他必须自己调用 Dispose。
  • @MystereMan 他问为什么 .NET 不处理句柄。原因是GC只关心内存。它对句柄一无所知,因此无法自动处理这些句柄。
  • 同样,虽然是真的,但它是无关紧要的.. 因为当通过调用 Dispose 的终结器对对象进行垃圾收集时,句柄将被释放。这里的问题与.net 知道任何句柄无关。假设在一个魔法世界中,.net 能够处理句柄,问题仍然是一样的,因为 gc 不会运行。他使用的类已经实现了 IDisposable,所以告诉他必须实现它是没有意义的。
  • 我没有告诉他他必须实现 IDisposable。我告诉他.NET 处理这些资源的方式是通过 IDisposable 模式和终结器。
【解决方案4】:

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

你可能想看看这个问题:What is meant by "managed" vs "unmanaged" resources in .NET?

【讨论】:

    【解决方案5】:

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

    这正是IDisposable 的用途。

    【讨论】:

      【解决方案6】:

      垃圾回收与您在代码中调用的 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

      【讨论】:

        【解决方案7】:

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

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

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

        【讨论】:

          猜你喜欢
          • 2017-06-20
          • 1970-01-01
          • 2020-03-10
          • 1970-01-01
          • 1970-01-01
          • 2015-10-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多