循环引用导致内存泄漏?

时间:2008-12-30 16:15:29

标签: .net memory memory-management memory-leaks circular-dependency

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

Parent通过控件集合引用子表单(子表单嵌入在那里)。子表格不是GC'd。

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

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

6 个答案:

答案 0 :(得分:38)

好问题!

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

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

因此,只有在“根”引用中的其他位置引用其中一个表单时 - 这两个表单才能安全地从GC中获取。

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

答案 1 :(得分:15)

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

答案 2 :(得分:12)

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

  1. 对全局对象的引用
  2. 对静态对象的引用
  3. 对静态字段的引用
  4. 将堆栈引用到本地对象
  5. 将堆栈引用到传递给方法的对象参数
  6. 对等待最终确定的对象的引用
  7. CPU寄存器中的引用到托管堆上的对象
  8. CLR维护活动根列表。垃圾收集器通过查看托管堆上的对象并查看应用程序仍可访问的对象(即可通过应用程序根目录访问)来工作。这样的对象被认为是根源。

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

答案 3 :(得分:5)

如果未引用父项和子项,但它们仅引用彼此,则会获得GCed。

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

答案 4 :(得分:2)

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

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

interface IEventSource
{
    event EventHandler SomethingHappened;
}

这是一个类的片段,用于处理来自该类型实例的事件。我们的想法是,无论何时向属性分配新实例,首先要取消订阅以前的任何分配,然后订阅新实例。空检查可确保正确的边界行为,更重要的是简化处理:您所做的只是将属性置零。

这提出了处置的重点。订阅事件的任何类都应实现IDisposable接口,因为事件是受管资源。 (N.B.为了简洁起见,我在示例中跳过了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;
    }

    // ...
}

答案 5 :(得分:0)

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