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

发布于 2024-08-07 07:55:06 字数 1735 浏览 8 评论 0原文

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

复制设置

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 子表单,以便其他表单可以被垃圾收集 - 但为什么我必须这样做呢?到底是怎么回事?

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

We've had problems with memory leaks in our application. I've managed to replicate one of the problems with the following simple example:

Replication setup

1) Create the following helper class which will be used to track object creation/destruction.

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

    public TestObject()
    {
        Count++;
    }

    ~TestObject()
    {
        Count--;
    }
}

2) Create an MDI form with three buttons, the first button will create a new MDI child as follows:

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

The second button will be used do the same, but with a non-MDI child form:

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

The third button will be used to garbage collect and then display how many TestObject instances are live:

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

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

Replication steps

1) Click Open MDI form button, then close the MDI form, then click the count button. It will return Count: 1. The MDI child form and the object it references was not garbage collected - something must still have a reference to it.

Also:

Click open MDI form three times, close all 3 forms, then click the count button. It will return Count: 1. It seems as though the last closed MDI child form is not garbage collected.

Counter-cases:

1) Click Open non-MDI form, close it. Then click the count button. It will return Count: 0, the form and object have been garbage collected.

Workaround

I can workaround this problem by doing this:

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

Before the garbage collection. This makes this dummy form the last closed MDI child form so that the other ones can be garbage collected - but why should I have to do this? What is going on?

Also it's a bit ugly as you will get a flicker of the form opening and closing, and it seems pretty hacky too.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

独自←快乐 2024-08-14 07:55:06

从技术上讲,因为 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

     地址 MT 大小
    01e2e960 001c650c 320  
    
  4. 接下来,运行!gcroot 01e2e960

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

     MT 字段偏移类型 VT 属性值名称
    6797ea24 40032a3 10 System.Int16 1 个实例 56 键
    6797ea24 40032a4 12 System.Int16 1 个实例 1 个掩码
    6798061c 40032a5 0 System.Object 0 实例 01e2e960 值 1
    
  6. 最后,我运行了 !name2ee System.Windows.Forms.dll System.Windows.Forms.Form ,然后运行了 !dumpclass 6604cb84 (由 ! name2ee)并查找 56。

     MT 字段偏移类型 VT 属性值名称
    67982c4c 4001e80 fd8 System.Int32 1 静态 56 PropFormerlyActiveMdiChild
    

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

Technically, because that Form is the "FormerlyActiveMdiChild". This looks like a bug. Fortunately, not a very serious one.

The ability to troubleshoot uncollected objects is a good skill to have. The windbg debugger from Microsoft that comes with the Debugging Tools for Windows (http://www.microsoft.com/whdc/devtools/debugging/default.mspx) is great for this purpose. In the walkthrough below, note that I have removed a lot of the output from windbg that is not pertinent.

  1. Instead of creating the MDI child instance of type Form, subclass it as TestChildForm to make it easy to identify.
  2. Start the executable and attach windbg. Load the .NET extensions with !loadby sos mscorwks.
  3. In windbg, run !dumpheap -type TestChildForm.

     Address       MT     Size
    01e2e960 001c650c      320  
    
  4. Next, run !gcroot 01e2e960.

    ESP:3de7fc:Root:01e29a78(System.EventHandler)->
    01e26504(WindowsFormsApplication1.Form1)->
    01e269b8(System.Windows.Forms.PropertyStore)->
    01e2ef04(System.Windows.Forms.PropertyStore+ObjectEntry[])
    
  5. Next, run !dumparray -details 01e2ef04 and search the output for 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. Finally, I ran !name2ee System.Windows.Forms.dll System.Windows.Forms.Form followed by !dumpclass 6604cb84 (as determined by !name2ee) and looked for 56.

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

If you would rather use the Visual Studio debugger instead of windbg, you must first enable Properties, Debug, Enable unmanaged code debugging. Substitute .load sos for .loadby sos mscorwks.

久隐师 2024-08-14 07:55:06

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

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

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

The reason why this is happening is quite simple there is still a reference to this form. The good news is that we can remove this reference.

Add an eventhandler for the form closing event.

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();
}

And a method to handle the event.

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

Here we reset the MdiParent property, by doing this the form is removed from the MdiChild list of the parent. Now when the form is closed this reference will reset as well.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文