一个 Windows Forms 应用程序中的内存泄漏问题

27
我们正在开发一个大型的.NET Windows Forms应用程序。我们遇到了内存泄漏/使用问题,尽管我们正在处理这些窗体。
情况如下:
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()等方法,但没有任何结果。

1
“等等”会延伸到多远?说实话,在这里你并没有谈论大量的内存,垃圾回收器可能甚至不会尝试在这些级别释放内存。 - Andrew Barber
1
你确定这是内存泄漏吗?在 Release 模式下编译时是否会发生?请注意,与 c++ 不同,.Net 不会立即释放您的窗体所占用的内存,而是将该内存保留供将来使用。尝试在长时间运行的情况下使用应用程序,并尝试最小化窗口(有时这会以某种方式强制释放)。 - jmservera
2
我认为你的意思是“60M到105M”,而不是“60K到105K”。150K微不足道。 - We Are All Monica
1
这不是泄漏,只是内存使用。 - leppie
4
我有同样的问题,内存使用量可能会增加到1.5GB(千兆字节)。 - Miguel
显示剩余8条评论
8个回答

30

寻找泄漏问题的第一步是检查事件处理程序,而不是缺少Dispose()调用。假设您的容器(即父窗体)加载了一个子窗体并添加了该子窗体的事件处理程序(ChildForm.CloseMe)。

如果子窗体将被清除以释放内存,则必须在它成为垃圾收集器的候选项之前移除此事件处理程序。


5
+1 -- 打败我了。这绝对是我在.NET中遇到的最常见的内存泄漏形式。 - Mark Simpson
4
没错。我们在开始收到“OutOfMemoryExceptions”报告时有大量的WinForms代码,这导致我们花费几个月的时间来查找这些泄漏类型。确实很痛苦!我仍然感到不安的是微软关于“AddHandler / +=”调用的文档没有一个巨大而闪烁的警告,即需要保证调用“RemoveHandler / -=”。 - STW
在.NET中,最常见的内存泄漏原因是+1。我希望微软在文档中更明显地展示void Dispose(bool disposing)模式(http://msdn.microsoft.com/en-us/library/fs2xkftw.aspx),以及事件处理程序的`+=`和`-=`。 - Travis Gockel
@Travis : 有趣的是你提到了 Dispose(bool)。我的第一个Stack Overflow问题之一也是关于它的:http://stackoverflow.com/questions/773165/why-does-vs2005-vb-net-implement-the-idisposable-interface-with-a-disposedisposi - STW
仅作记录,由于垃圾回收(GC)处理循环引用,除非您有一个根在表单控件层次结构之外的EventHandler,否则不需要显式地删除处理程序(-=)。 - Jeff
@Jeff -- 正确;只要对象图中的整个部分不可达,就可以收集它们。然而,我建议添加额外的 RemoveHandler / -= 调用,以帮助保险起见--特别是如果 GC 不是很理解。 - STW

12
在Windows Forms应用程序中,最常见的内存泄漏源是在窗体销毁后仍保持连接的事件处理程序...因此这是开始调查的好地方。像http://memprofiler.com/这样的工具可以帮助确定从未被GC回收的实例的根源。
至于对GC.Collect的调用:
1. 在实际应用中,绝对不要这样做。 2. 为了确保你的GC collect真正尽可能多地回收,需要进行多次遍历和等待挂起的终结器,以便调用真正是同步的。
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;
}

2
直到第二次GC调用或等待挂起的终结器之前,它可能不会在任务管理器中反映已释放的内存。关于在调用ShowDialog后进行Dispose的说法是不正确的。请参阅Microsoft文章http://msdn.microsoft.com/en-us/library/system.windows.forms.form.showdialog%28VS.90%29.aspx下的“为什么必须在调用ShowDialog()后Dispose()”。 - Jeff
1
你提到了任务管理器,它是一种糟糕的测量.NET内存使用情况的方法。使用perfmon的进程指标来测量私有字节等内容。任务管理器曾经表明通过最小化和恢复应用程序可以达到约95%的内存减少,这正说明了其报告是多么的不准确。 - STW
是的,我知道“最小化窗口后你的应用程序会立即变得非常节省内存”的效果 :) - Jeff
你是正确的,ShowDialog()只有在窗体关闭时才会隐藏;我撤回我的评论。然而,在方法中最后一次引用窗体后,窗体仍然变得可以被收集。 - Dan Bryant
我花了四年时间进行WinForms编程才意识到ShowDialog()的作用...而且只有非常痛苦的经历才教会了我这一点。 :D - Jeff

12

释放表单并不一定保证您不会泄漏内存。例如,如果您将其绑定到数据集但在完成后没有释放数据集,则可能会出现泄漏。您可能需要使用分析工具来识别未被释放的可清除资源。

顺带一提,调用GC.Collect()是一个坏主意。只是说一下。


4
如果频繁调用GC.Collect(),那通常是糟糕编程的证据(即使坏编程只是GC.Collect()调用本身)。请注意,这里涉及到的仅为技术翻译,无法提供上下文或解释。 - Andrew Barber
1
@Andrew:你无法想象在什么情况下调用GC.Collect()会是一个好主意吗? - Hogan
1
@Hogan:如果我能够想到任何情况下都不需要调用GC.Collect()的话,我就会使用“总是”而不是“频繁”。 - Andrew Barber
3
@Hogan -- 这些准则非常明确:不要这样做!当然,有一些合理的理由可以这样做,但这些理由只适用于极少数的应用程序(这一少数是如此之小,以至于您可以安全地假设您不在其中)。所以,请再次强调 不要这样做。垃圾回收是一项非常昂贵且缓慢的操作(特别是在 .NET 4 之前),它还是一个完全自动化的过程,因此手动调用它是浪费的。 - STW
2
@Hogan - 在另一个答案中指出,由于代降级,调用GC.Collect()可能会使情况变得更糟。 - Otávio Décio
显示剩余2条评论

1

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

这也可能是巧合--我相信.NET在段中分配内存,因此您可能不会看到每个表单释放时工作集下降的情况(内存由表单释放,但仍为应用程序的下一个分配保留)。由于您的内存分配至少有一层抽象,因此您不会总是获得您的工作集上下移动与您分配的确切字节数相同的行为。

测试的方法是创建大量的表单实例并释放它们--尝试放大泄漏,以便您分配和释放数百个实例。您的内存是否继续上升而不下降(如果是,则存在问题),还是最终返回接近正常?(可能没有问题)。


1

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


0

请确保您已经完全删除了所有对该表单的引用。有时候可能会出现一些您没有注意到的隐藏引用。

例如:如果您从对话框中连接到外部事件,即外部窗口的事件,如果您忘记从它们中断开连接,那么您将会有一个剩余的对该表单的引用,这个引用永远不会消失。

在您的对话框中尝试以下代码(示例糟糕的代码...):

   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(仅用于释放非托管资源,例如:关闭文件等),表单仍然保留在内存中。

0

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

1)你的表单关闭了,但还有一个引用挂着,不能被垃圾回收。

2)你加载了一些资源,但没有释放它们

3)你正在使用XSLT,并在每次转换时编译它

4)你有一些定制的代码是在运行时编译和加载的


1
此外,.NET GC 以“代”的形式工作;即使您100%确定没有剩余引用到表单或其他对象,调用GC.Collect()也不能保证内存将被释放。幸存的对象可能会晋升到更高的代,这意味着它们将不经常被检查以进行回收。 - Andrew Barber
@Andrew - 非常好的观点。我听说过调用GC.Collect()会使情况变得更糟,正是因为晋升的原因。 - Otávio Décio
可能这就是你想要的,但是对于 XmlSerializer 的某些构造函数而言,它们每次被调用时还会动态生成并加载一个新的程序集--如果不手动缓存和检索它们的结果,那么它们很快就会成为一个泄漏。 - STW
第一代只有2MB(尽管现在可能因机器而异) - Aliostad

0

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


我们正在使用Telerik控件。 - Kashif

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接