【问题标题】:Why does the last MDI child form that was closed not get garbage collected?为什么最后一个关闭的 MDI 子窗体没有被垃圾收集?
【发布时间】:2009-10-06 16:14:09
【问题描述】:

我们的应用程序中存在内存泄漏问题。我已经设法通过以下简单示例复制了其中一个问题:

复制设置

1) 创建以下帮助类,用于跟踪对象的创建/销毁。

public class TestObject
{
    public static int Count { get; set; }

    public TestObject()
    {
        Count++;
    }

    ~TestObject()
    {
        Count--;
    }
}

2) 创建一个包含三个按钮的 MDI 表单,第一个按钮将创建一个新的 MDI 子项,如下所示:

    private void ctlOpenMDI_Click(object sender, EventArgs e)
    {
        Form newForm = new Form();
        newForm.MdiParent = this;
        newForm.Tag = new TestObject();
        newForm.Show();
    }

第二个按钮也将用于做同样的事情,但使用的是非 MDI 子窗体:

    private void ctlOpenNonMDIForm_Click(object sender, EventArgs e)
    {
        Form newForm = new Form();
        newForm.Tag = new TestObject();
        newForm.Show();
    }

第三个按钮将用于垃圾收集,然后显示有多少 TestObject 实例处于活动状态:

    private void ctlCount_Click(object sender, EventArgs e)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();

        MessageBox.Show("Count: " + TestObject.Count);
    }

复制步骤

1) 点击打开 MDI 表单按钮,然后关闭 MDI 表单,然后点击计数按钮。它将返回 Count: 1. MDI 子窗体及其引用的对象没有被垃圾回收 - 某些东西必须仍然具有对它的引用。

还有:

点击打开 MDI 表单三次,关闭所有 3 个表单,然后点击计数按钮。它将返回 Count: 1。似乎最后关闭的 MDI 子窗体没有被垃圾回收。

反例:

1) 点击打开非MDI表单,关闭它。然后单击计数按钮。它将返回 Count: 0,表单和对象已被垃圾回收。

解决方法

我可以通过这样做来解决这个问题:

        Form form = new Form();
        form.MdiParent = this;
        form.Show();
        form.Close();

在垃圾收集之前。这使得这个虚拟表单成为最后一个关闭的 MDI 子表单,以便其他表单可以被垃圾收集 - 但我为什么必须这样做?怎么回事?

而且它有点难看,因为你会看到表单打开和关闭的闪烁,而且看起来也很hacky。

【问题讨论】:

    标签: c# winforms forms memory-leaks mdichild


    【解决方案1】:

    从技术上讲,因为 Form 是“FormerlyActiveMdiChild”。这看起来像一个错误。幸运的是,不是很严重。

    对未收集的物品进行故障排除的能力是一项很好的技能。 Microsoft 的windbg 调试器附带Windows 调试工具(http://www.microsoft.com/whdc/devtools/debugging/default.mspx) 非常适合此用途。在下面的演练中,请注意我已经从 windbg 中删除了很多不相关的输出。

    1. 不要创建 Form 类型的 MDI 子实例,而是将其子类化为 TestChildForm 以便于识别。
    2. 启动可执行文件并附加windbg。使用 !loadby sos mscorwks 加载 .NET 扩展。
    3. 在windbg中,运行!dumpheap -type TestChildForm

       Address       MT     Size
      01e2e960 001c650c      320  
      
    4. 接下来,运行!gcroot 01e2e960

      ESP:3de7fc:Root:01e29a78(System.EventHandler)->
      01e26504(WindowsFormsApplication1.Form1)->
      01e269b8(System.Windows.Forms.PropertyStore)->
      01e2ef04(System.Windows.Forms.PropertyStore+ObjectEntry[])
      
    5. 接下来,运行!dumparray -details 01e2ef04 并在输出中搜索01e2e960

            MT    Field   Offset                 Type VT     Attr    Value Name
      6797ea24  40032a3       10         System.Int16  1 instance       56 Key
      6797ea24  40032a4       12         System.Int16  1 instance        1 Mask
      6798061c  40032a5        0        System.Object  0 instance 01e2e960 Value1
      
    6. 最后,我运行!name2ee System.Windows.Forms.dll System.Windows.Forms.Form,然后运行!dumpclass 6604cb84(由!name2ee确定)并寻找56。

            MT    Field   Offset                 Type VT     Attr    Value Name
      67982c4c  4001e80      fd8         System.Int32  1   static       56 PropFormerlyActiveMdiChild
      

    如果您更愿意使用 Visual Studio 调试器而不是 windbg,则必须首先启用属性、调试、启用非托管代码调试。将.load sos 替换为.loadby sos mscorwks

    【讨论】:

    • 优秀的答案!搜索该属性“FormerlyActiveMdiChild”似乎确实表明它是最近引入的 Microsoft 错误。谢谢。
    【解决方案2】:

    发生这种情况的原因很简单,仍然有对这个表单的引用。好消息是我们可以删除此引用。

    为表单关闭事件添加事件处理程序。

    private void ctlOpenMDI_Click(object sender, EventArgs e)
    {
        Form newForm = new Form();
        newForm.FormClosing += new FormClosingEventHandler(form_Closing);
        newForm.MdiParent = this;
        newForm.Tag = new TestObject();
        newForm.Show();
    }
    

    以及处理事件的方法。

    private void form_Closing(object sender, EventArgs e)
    {
        Form form = sender as Form;
        form.MdiParent = null;
    }
    

    这里我们重置了 MdiParent 属性,通过这样做,表单从父级的 MdiChild 列表中删除。现在,当表单关闭时,此引用也将重置。

    【讨论】:

    • 这并不能解决这个问题。
    猜你喜欢
    • 2011-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-19
    • 2014-08-08
    • 1970-01-01
    • 2021-06-04
    相关资源
    最近更新 更多