C# 为什么最后一个关闭的MDI子窗体没有被垃圾回收?

C# 为什么最后一个关闭的MDI子窗体没有被垃圾回收?,c#,winforms,forms,memory-leaks,mdichild,C#,Winforms,Forms,Memory Leaks,Mdichild,我们的应用程序中存在内存泄漏问题。我通过以下简单示例成功地复制了其中一个问题: 复制设置 1) 创建以下帮助器类,该类将用于跟踪对象的创建/销毁 public class TestObject { public static int Count { get; set; } public TestObject() { Count++; } ~TestObject() { Count--; } } 2) 使

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

复制设置

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表单三次,关闭所有三个表单,然后单击计数按钮。它将返回Count:1。似乎最后一个关闭的MDI子窗体没有被垃圾收集

反案件:

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

解决方法

我可以通过以下方法解决此问题:

        Form form = new Form();
        form.MdiParent = this;
        form.Show();
        form.Close();
在垃圾收集之前。这使得这个虚拟表单成为最后一个关闭的MDI子表单,以便可以对其他子表单进行垃圾收集—但是为什么我必须这样做呢?发生了什么事


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

从技术上讲,因为
表单
就是“FormerlyActiveMdiChild”。这看起来像个虫子。幸运的是,这不是一个非常严重的问题

对未收集的对象进行故障排除的能力是一项很好的技能。Windows调试工具()附带的Microsoft windbg调试器非常适合此用途。在下面的演练中,请注意,我已经从windbg中删除了许多不相关的输出

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

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

    ESP:3de7fc:Root:01e29a78(System.EventHandler)->
    01e26504(WindowsFormsApplication1.Form1)->
    01e269b8(System.Windows.Forms.PropertyStore)->
    01e2ef04(System.Windows.Forms.PropertyStore+ObjectEntry[])
    
          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
    
  • 接下来,运行
    !dumparray-详细信息01e2ef04
    ,并在输出中搜索
    01e2e960

    ESP:3de7fc:Root:01e29a78(System.EventHandler)->
    01e26504(WindowsFormsApplication1.Form1)->
    01e269b8(System.Windows.Forms.PropertyStore)->
    01e2ef04(System.Windows.Forms.PropertyStore+ObjectEntry[])
    
          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
    
  • 最后,我运行了
    !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

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

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

    回答得很好!搜索该属性“FormerlyActivityChild”似乎确实表明它是最近引入的Microsoft错误。谢谢。这并不能解决这个问题。