38

我正在尝试解决 Windows 窗体应用程序中的内存泄漏问题。我现在正在查看一个包含多个嵌入式表单的表单。让我担心的是,子表单在其构造函数中引用了父表单,并将其保存在私有成员字段中。所以在我看来,垃圾收集时间到了:

父通过控件集合引用子表单(子表单嵌入其中)。子表单不是 GC'd。

子表单通过私有成员字段引用父表单。父表单不是 GC'd。

这是对垃圾收集器如何评估情况的准确理解吗?有什么方法可以“证明”它以用于测试目的?

4

6 回答 6

44

好问题!

不,这两种形式都将(可以)进行 GC,因为 GC 不会直接在其他参考中查找参考。它只查找所谓的“根”引用......这包括堆栈上的引用变量,(变量在堆栈上,实际对象当然在堆上),CPU 寄存器中的引用变量,以及引用变量类中的静态字段...

所有其他引用变量只有在上述过程找到的“根”引用对象之一的属性中被引用时才被访问(和 GC'd)......(或在由根对象中的引用引用的对象中) , ETC...)

因此,只有当其中一种形式在“根”引用中的其他地方被引用时——那么这两种形式都不会受到 GC 的影响。

我能想到的“证明”它的唯一方法(不使用内存跟踪实用程序)是在方法内的循环中创建几十万个这样的表单,然后在方法中查看应用程序的内存占用,然后退出方法,调用GC,再看一下footprint。

于 2008-12-30T16:23:15.540 回答
15

正如其他人已经说过的,GC 在循环引用方面没有问题。我想补充一点,在 .NET 中泄漏内存的常见地方是事件处理程序。如果您的一个表单有一个附加到另一个“活动”对象的事件处理程序,那么就会有对您的表单的引用并且该表单不会被 GC'd。

于 2008-12-30T16:45:21.843 回答
12

垃圾收集通过跟踪应用程序根来工作。应用程序根是包含对托管堆上的对象(或 null)的引用的存储位置。在 .NET 中,根是

  1. 对全局对象的引用
  2. 对静态对象的引用
  3. 对静态字段的引用
  4. 堆栈上对本地对象的引用
  5. 堆栈上对传递给方法的对象参数的引用
  6. 对等待完成的对象的引用
  7. CPU 寄存器中对托管堆上对象的引用

活动根列表由 CLR 维护。垃圾收集器通过查看托管堆上的对象并查看应用程序仍然可以访问哪些对象来工作,也就是说,可以通过应用程序根访问。这样的对象被认为是有根的。

现在假设您有一个包含对子表单的引用的父表单,并且这些子表单包含对父表单的引用。此外,假设应用程序不再包含对父表单或任何子表单的引用。然后,出于垃圾收集器的目的,这些托管对象不再是 root 并且将在下次垃圾收集发生时进行垃圾收集。

于 2008-12-30T16:48:22.680 回答
5

如果父母和孩子都没有被引用,但他们只是互相引用,他们确实会被 GCed。

获取内存分析器来真正检查您的应用程序并回答您的所有问题。我可以推荐http://memprofiler.com/

于 2008-12-30T16:23:55.663 回答
2

我想回应 Vilx 关于事件的评论,并推荐一种有助于解决它的设计模式。

假设您有一个类型是事件源,例如:

interface IEventSource
{
    event EventHandler SomethingHappened;
}

这是处理来自该类型实例的事件的类的片段。这个想法是,每当您将新实例分配给属性时,您首先取消订阅任何先前的分配,然后订阅新实例。空检查确保正确的边界行为,更重要的是,简化了处置:您所做的就是将属性设为空。

这就提出了处置的问题。任何订阅事件的类都应该实现 IDisposable 接口,因为事件是托管资源。(注意,为了简洁起见,我在示例中跳过了 Dispose 模式的正确实现,但你明白了。)

class MyClass : IDisposable
{
    IEventSource m_EventSource;
    public IEventSource EventSource
    {
        get { return m_EventSource; }
        set
        {
            if( null != m_EventSource )
            {
                m_EventSource -= HandleSomethingHappened;
            }
            m_EventSource = value;
            if( null != m_EventSource )
            {
                m_EventSource += HandleSomethingHappened;
            }
        }
    }

    public Dispose()
    {
        EventSource = null;
    }

    // ...
}
于 2011-06-24T20:34:11.587 回答
0

GC 可以正确处理循环引用,如果这些引用是保持表单活动的唯一事物,那么它们将被收集。
.net 没有从表单中回收内存,我遇到了很多麻烦。在 1.1 中,menitem 存在一些错误(我认为),这意味着它们没有被处理掉并且可能会泄漏内存。在这种情况下,添加显式调用 dispose 并清除表单的 Dispose 方法中的成员变量可以解决问题。我们发现这也有助于为其他一些控件类型回收内存。
我还花了很长时间使用 CLR 分析器来研究为什么没有收集表单。据我所知,框架保留了引用。每个表单类型一个。因此,如果您创建 100 个 Form1 实例,然后将它们全部关闭,则只有 99 个会被正确回收。我没有找到任何方法来治愈这个。
我们的应用程序已经转移到 .net 2,这似乎要好得多。当我们打开第一个表单时,我们的应用程序内存仍然会增加,并且在关闭时不会回退,但我相信这是因为 JIT 代码和加载了额外的控制库。
我还发现,虽然 GC 可以处理循环引用,但循环事件处理程序引用似乎(有时)存在问题。IE object1 引用 object2 并且 object1 具有处理来自 object2 的事件的方法。我发现这样的情况并没有在我预期的时候释放对象,但我永远无法在测试用例中重新生成它。

于 2008-12-30T16:38:16.383 回答