为什么关闭的最后一个MDI子窗体不会被垃圾收集?

时间:2009-10-06 16:14:09

标签: c# winforms forms memory-leaks mdichild

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

复制设置

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。

2 个答案:

答案 0 :(得分:3)

从技术上讲,因为Form是“FormerlyActiveMdiChild”。这看起来像一个bug。幸运的是,不是一个非常严肃的人。

对未收集的对象进行故障排除的能力是一项很好的技能。 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
    
  7. 如果您更愿意使用Visual Studio调试器而不是windbg,则必须先启用“属性”,“调试”,“启用非托管代码调试”。将.load sos替换为.loadby sos mscorwks

答案 1 :(得分:0)

之所以发生这种情况非常简单,仍然可以参考这种形式。好消息是我们可以删除此引用。

为表单结束事件添加eventhandler。

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列表中删除。现在,当表单关闭时,此引用也将重置。