循环引用导致内存泄漏?

Posted

技术标签:

【中文标题】循环引用导致内存泄漏?【英文标题】:Circular References Cause Memory Leak? 【发布时间】:2010-09-28 21:06:48 【问题描述】:

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

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

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

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

【问题讨论】:

【参考方案1】:

好问题!

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

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

因此,只有当其中一个表单在“根”引用中的其他位置被引用时——那么这两种表单都不会受到 GC 的影响。

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

【讨论】:

或者只是在每个表单中分配一个巨大的缓冲区。【参考方案2】:

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

【讨论】:

【参考方案3】:

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

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

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

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

【讨论】:

@Jason,“对象参数”是什么意思?而且我相信引用的位置是关键的决定因素……如果在堆栈上,或者类的静态成员,或者在 CPU 寄存器中,那么它就是根引用。 ...否则不会。 (freachable queue 除外,--另一个话题)【参考方案4】:

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

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

【讨论】:

【参考方案5】:

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

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

interface IEventSource

    event EventHandler SomethingHappened;

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

这带来了处置点。任何订阅事件的类都应该实现 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;
    

    // ...

【讨论】:

【参考方案6】:

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

【讨论】:

以上是关于循环引用导致内存泄漏?的主要内容,如果未能解决你的问题,请参考以下文章

🔥🔥造成循环引用和内存泄漏的几种情况

js晋级篇——前端内存泄漏探讨

Block循环引用问题(Objective-c)

【OC语法】block的循环引用

python外篇(内存泄露)

iOS 内存管理