循环引用会导致内存泄漏吗?

39
我正在查找Windows窗体应用程序中的内存泄漏问题。现在我正在查看一个包含多个嵌入式窗体的窗体。令我担忧的是,子窗体在构造函数中会获取父窗体的引用,并将其保存在私有成员字段中。因此,在垃圾回收时间到来时:
- 父窗体通过控件集合(子窗体嵌入其中)持有对子窗体的引用。子窗体无法被垃圾回收。 - 子窗体通过私有成员字段持有对父窗体的引用。父窗体无法被垃圾回收。
这是我对垃圾回收器如何评估这种情况的准确理解吗?有没有任何方法可以进行“证明”以进行测试?
6个回答

46

很好的问题!

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

所有其他引用变量只有在它们被引用于上述过程发现的一个“根”引用对象的属性中时才会被访问(和垃圾回收)...(或者在由根对象引用的对象或其他引用中引用的对象中...)

因此,只有当其中一种形式在“根”引用的其他地方被引用时,这两种形式才会免受垃圾回收的影响。

我能想到的唯一一种“证明”它的方法(不使用内存跟踪工具)是在方法内创建几十万个这些表单,并在循环内部,同时查看应用程序的内存占用情况,然后退出该方法,调用GC,再次查看内存占用情况。


2
或者在每个表单内分配一个大缓冲区。 - Gusdor

15

正如其他人所说,垃圾回收在循环引用方面没有问题。我想补充一点的是,在.NET中泄漏内存的常见地方是事件处理程序。如果你的一个窗体附加了对另一个“活动”对象的事件处理程序,则会有一个对你的窗体的引用,因此该窗体将无法被GC回收。


12

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

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

CLR维护活动根列表。垃圾收集器会检查托管堆上的对象,并查看哪些对象仍然可以通过应用程序访问,即可通过应用程序根访问。这样的对象被视为已根据。

现在假设您有一个父窗体,其中包含对子窗体的引用,这些子窗体包含对父窗体的引用。此外,假设应用程序不再包含对父窗体或任何子窗体的引用。那么,对于垃圾收集器而言,这些托管对象不再是根,并将在下次垃圾收集发生时被回收。


@Jason,"对象参数"是什么意思?我认为引用的位置是关键因素...如果在堆栈上,或者是类的静态成员,或者在CPU寄存器中,则它是根引用。...否则不是。(除了可回收队列-另一个话题) - Charles Bretana

5
如果父对象和子对象都没有被引用,但它们只相互引用,那么它们会被垃圾回收。建议使用内存分析器来检查您的应用程序并回答所有问题。我可以推荐http://memprofiler.com/

2
我想回应Vilx有关事件的评论,并推荐一种设计模式来解决它。
假设您有一个类型,它是一个事件源,例如:
interface IEventSource
{
    event EventHandler SomethingHappened;
}

这是一个处理该类型实例事件的类片段。其思想是,每当您将新实例分配给属性时,首先取消订阅任何先前的分配,然后订阅新实例。空值检查确保正确的边界行为,并更重要的是简化处理:您只需将属性设置为 null。
这带来了处理的问题。任何订阅事件的类都应该实现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;
    }

    // ...
}

0
GC可以正确处理循环引用,如果这些引用是唯一使表单保持活动状态的内容,那么它们将被收集。
我曾经遇到过很多.NET无法从表单中回收内存的问题。在1.1版本中,有一些关于菜单项(我想)的错误,这意味着它们没有被处理并且可能会泄漏内存。在这种情况下,在表单的Dispose方法中添加显式调用dispose和清除成员变量可以解决问题。我们发现这也有助于回收一些其他控件类型的内存。
我还花了很长时间使用CLR分析器来查看为什么表单没有被回收。据我所知,引用被框架保留。每个表单类型一个引用。因此,如果您创建100个Form1实例,然后关闭它们所有,只有99个会被正确回收。我没有找到任何解决方法。
我们的应用程序已经转移到.NET 2,这似乎要好得多。我们的应用程序内存仍然会在打开第一个表单时增加,并且在关闭后不会回落,但我认为这是由于JIT编译的代码和加载的额外控件库造成的。
我还发现,尽管GC可以处理循环引用,但它似乎在处理循环事件处理程序引用时存在问题(有时)。即object1引用object2,并且object1具有处理来自object2的事件的方法。我发现在某些情况下,这不会在我预期的时间释放对象,但我从未能够在测试用例中重现它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接