【问题标题】:Memory leaks in a Windows Forms applicationWindows 窗体应用程序中的内存泄漏
【发布时间】:2011-04-22 22:30:00
【问题描述】:

我们正在开发一个大型 .NET Windows 窗体应用程序。尽管我们正在处理表单,但我们仍面临内存泄漏/使用问题。

场景是这样的:

  1. 我们的应用程序正在使用 60 KB 内存,并在网格中显示记录列表。
  2. 当用户单击记录时,它会打开一个表单myform.showDialog,显示详细信息。内存跃升从 60 KB 到 105 MB。
  3. 现在我们关闭表单myform 以返回网格,然后dispose该表单并将其设置为null。内存保持为 105 MB。
  4. 现在如果我们再次执行第 2 步,它会从 105 MB 跳到 150 MB 等等。

关闭myForm时如何释放内存?

我们已经尝试过GC.Collect()等,但没有任何结果。

【问题讨论】:

  • “等等”走多远?老实说,您在这里谈论的不是大量内存,GC 甚至可能不会尝试在这些级别释放内存。
  • 您确定这是泄漏吗?在发布模式下编译时会发生这种情况吗?认为 .Net 不会像在 C++ 中那样在释放表单的同时释放内存,它会保留内存以供将来使用。尝试在更长的会话中使用应用程序,并尝试最小化窗口(这有时会以某种方式强制释放)。
  • 我认为您的意思是“60M 到 105M”而不是“60K 到 105K”。 150K 不算什么。
  • 这不是泄漏,只是内存使用情况。
  • 我有同样的问题,内存使用量可以增长到 1.5 Gb (Gigabyte)

标签: c# .net winforms memory-leaks c#-3.0


【解决方案1】:

寻找泄漏的第一个地方是事件处理,而不是丢失Dispose() 调用。假设您的容器(父表单)加载了一个子表单并为该子表单的事件添加了一个处理程序 (ChildForm.CloseMe)。

如果要从内存中清除子窗体,则必须先删除此事件处理程序,然后才能将其作为垃圾回收的候选对象。

【讨论】:

  • +1 -- 打败我。这绝对是我在 .NET 中遇到的最常见的内存泄漏形式
  • 是的。当我们开始收到OutOfMemoryExceptions 的报告时,我们有 吨 的 WinForms 代码,这导致了数月的时间来寻找这些类型的泄漏。绝对是痛!我仍然感到不安的是,MS 关于AddHandler / += 调用的文档没有一个关于需要保证调用RemoveHandler / -= 的巨大闪烁警告
  • +1 表示 .NET 中内存泄漏的最常见原因。我希望 MS 在文档中使 void Dispose(bool disposing) 模式 (msdn.microsoft.com/en-us/library/fs2xkftw.aspx) 以及事件处理程序的 += 和 -= 更加明显。
  • @Travis:有趣的是你应该提到Dispose(bool)。我的第一个 SO 问题是关于这个的:stackoverflow.com/questions/773165/…
  • 请注意,由于 GC 确实处理循环引用,除非您有一个植根于表单控件层次结构之外的 EventHandler,否则实际上没有必要显式删除您的处理程序 (-=)
【解决方案2】:

处理表单不一定保证您不会泄漏内存。例如,如果您将其绑定到数据集,但在完成后没有处理数据集,则可能会发生泄漏。您可能需要使用分析工具来确定哪些一次性资源没有被释放。

顺便说一句,调用 GC.Collect() 是个坏主意。只是说说而已。

【讨论】:

  • +1 GC.Collect() 被调用经常是糟糕的编程的证据(即使糟糕的编程只是 GC.Collect() 调用本身)
  • @Andrew :您可以想象调用 GC.Collect() 不会是一个好主意的情况?
  • @Hogan : 如果我可以设想 no 的情况来调用GC.Collect(),我会使用 always 这个词而不是 经常.
  • @Hogan -- 指导方针非常明确:不要!当然,这样做有正当理由,但这些理由存在于极少数应用程序中(如此之少的应用程序,您可以放心地假设您不在其中)。所以,再次不要!。 GC 是一个非常昂贵且缓慢的操作(尤其是在 .NET 4 之前),它也是一个完全自动化的过程——因此手动调用它是一种浪费。
  • @Hogan - 在另一个答案中指出,由于代提升,调用 GC.Collect() 可能会使事情变得更糟。
【解决方案3】:

Windows 窗体应用程序中最常见的内存泄漏源是在处理表单后仍保持连接状态的事件处理程序...所以这是开始调查的好地方。 http://memprofiler.com/ 之类的工具可以极大地帮助确定永远不会被 GC 的实例的根。

至于您对 GC.Collect 的调用

  1. 在实践中这样做肯定不好
  2. 为了确保您的 GC 收集确实尽可能多地收集,您需要进行多次传递并等待挂起的终结器,因此调用是真正同步的。

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
    GC.WaitForPendingFinalizers();
    

此外,任何保留在您的表单实例上的东西都会将表单保留在内存中,即使在它被关闭和处置之后也是如此。

喜欢:

static void Main() {
    var form = new MyForm();
    form.Show();
    form.Close();

    // The GC calls below will do NOTHING, because you still have a reference to the form!
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
    GC.WaitForPendingFinalizers();

    // Another thing to not: calling ShowDialog will NOT 
    // get Dispose called on your form when you close it.
    var form2 = new MyForm();
    DialogResult r = form2.ShowDialog();

    // You MUST manually call dispose after calling ShowDialog! Otherwise Dispose 
    // will never get called.
    form2.Dispose();

    // As for grids, this will ALSO result in never releasing the form in
    // memory, because the GridControl has a reference to the Form itself
    // (look at the auto-generated designer code).
    var form3 = new MyForm();
    form3.ShowDialog();
    var grid = form3.MyGrid;

    // Note that if you're planning on actually using your datagrid
    // after calling dispose on the form, you're going to have
    // problems, since calling Dipose() on the form will also call
    // dispose on all the child controls.
    form3.Dispose();
    form3 = null;
}

【讨论】:

  • 它可能不会在任务管理器中反映释放的内存,直到第二次 GC 调用或等待挂起的终结器。您调用 ShowDialog 后关于 Disposing 的声明不正确。请参阅 Microsoft 文章 msdn.microsoft.com/en-us/library/… 下的“为什么在调用 ShowDialog() 后必须 Dispose()”
  • 您提到了任务管理器,它是一种测量 .NET 内存使用情况的可怕方法。将perfmon 的进程指标用于私有字节等内容。过去,任务管理器通过最小化和恢复应用程序向我们展示了大约 95% 的内存减少,这证明了它的报告是多么的不合格。
  • 是的,我知道“最小化并且突然你的应用程序是超大内存保守”效果:)
  • 你说得对, ShowDialog() 只在表单关闭时隐藏;我收回我的评论。但是,在方法中最后一次引用表单之后,它仍然可以被收集。
  • 我花了四年的时间进行 WinForms 编程,才意识到 ShowDialog() 的意义……而这只是一次非常痛苦的经历教会了我。 :D
【解决方案4】:

我最近遇到了一个类似的问题,即运行计时器将表单保留在内存中,即使表单已关闭。解决方案是在关闭表单之前停止计时器。

【讨论】:

    【解决方案5】:

    我没有看到你的代码,但这是最有可能的情况:

    1) 您的表单已关闭,但它的引用挂在那里,无法被垃圾收集。

    2) 您正在加载一些没有被释放的资源

    3) 您正在使用 XSLT 并在每次转换时对其进行编译

    4) 您有一些在运行时编译和加载的自定义代码

    【讨论】:

    • +1 此外,.NET GC 可以“世代”工作;即使您 100% 确定没有对表单或其他对象的剩余引用,调用 GC.Collect() 也不能保证内存将被释放。在 GC 中幸存下来的对象可以提升到更高的一代,这意味着它们会被更少地检查以进行收集。
    • @Andrew - 非常好。我听说过调用 GC.Collect() 使情况变得更糟的情况正是因为晋升。
    • 这可能是你得到的,但是XmlSerializer 的某些构造函数也会在每次调用它们时动态生成并加载一个新程序集——如果你不手动缓存并检索他们的结果,然后他们很快就会成为泄漏
    • 第一代只有 2 MB(虽然现在它可能因机器而异)
    【解决方案6】:

    某些第三方控件的代码存在错误。如果您使用其中一些控件,这可能不是您的问题。

    【讨论】:

    • 我们正在使用 Telerik 控件。
    【解决方案7】:

    检查您是否已完全删除了对表单的所有引用。有时可能会发生一些您没有注意到的隐藏引用。

    例如:如果您从对话框附加到外部事件,即附加到外部窗口的事件,如果您忘记从它们中分离,您将拥有一个永远不会消失的对表单的剩余引用。

    在您的对话框中尝试此代码(示例错误代码...):

       protected override void OnLoad(EventArgs e)
       {
           Application.OpenForms[0].Activated += new EventHandler(Form2_Activated);
           base.OnLoad(e);
       }
    
       void Form2_Activated(object sender, EventArgs e)
       {
           Console.WriteLine("Activated!");
       }        
    

    并且多次打开和关闭对话框,您将看到控制台中的字符串数量在每次调用时都会增加。这意味着即使您调用 dispose,表单仍保留在内存中(仅应用于释放非托管资源,即:关闭文件和类似的事情)。

    【讨论】:

      【解决方案8】:

      首先,检查对您的表单的引用。您的表单是否订阅了任何事件?这些都算作引用,如果事件发布者的寿命比您的表单长,那么它将保留您的表单(除非您取消订阅)。

      这也可能是一个巧合——我相信 .NET 会分段分配内存,因此您可能不会看到您的工作集随着每次表单发布而下降(内存由您的表单释放,但仍保留为您的应用程序的下一次分配)。由于您的内存分配至少是一个抽象,因此您不会总是得到工作集按照您分配的确切字节数上下波动的行为。

      一种测试方法是创建 大量 表单实例并释放它们 - 尝试放大泄漏,以便分配和释放 100 个实例。您的记忆力是否会继续增加而不会下降(如果是,您有问题),还是最终会恢复到接近正常水平? (可能不是问题)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-11-02
        • 1970-01-01
        • 1970-01-01
        • 2013-04-16
        • 1970-01-01
        • 2017-03-29
        相关资源
        最近更新 更多