.net 循环引用导致内存泄漏?

.net 循环引用导致内存泄漏?,.net,memory,memory-management,memory-leaks,circular-dependency,.net,Memory,Memory Management,Memory Leaks,Circular Dependency,我正在尝试运行windows窗体应用程序中的内存泄漏。我现在看的是一个包含多个嵌入表单的表单。我担心的是,子窗体在其构造函数中引用父窗体,并将其保存在私有成员字段中。所以在我看来,垃圾收集时间到了: 父窗体通过控件集合(子窗体嵌入其中)引用子窗体。子窗体不是GC'd 子窗体通过私有成员字段引用父窗体。父窗体不是GC'd 这是对垃圾收集器将如何评估情况的准确理解吗?有什么方法可以“证明”它用于测试吗?好问题 不,这两个表单都是(可以)GC'd,因为GC不会直接在其他引用中查找引用。它只查找所谓的“

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

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

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

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

好问题

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

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

因此,只有当其中一个表单在“根”引用中的其他地方被引用时,这两个表单才不会受到GC的影响


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

如果父项和子项都未被引用,但它们仅相互引用,则它们会获得GCD


找一个内存分析器来真正检查你的应用程序并回答你的所有问题。我建议GC可以正确处理循环引用,如果这些引用是保持表单活动的唯一因素,那么它们将被收集。
由于.net不能从表单中回收内存,我遇到了很多麻烦。在1.1中,menuitem(我想)周围有一些bug,这意味着它们没有得到处理,可能会泄漏内存。在这种情况下,添加一个显式的dispose调用并清除表单的dispose方法中的成员变量可以解决问题。我们发现,这也有助于为其他一些控件类型回收内存。
我还花了很长时间与CLR profiler一起研究为什么不收集表单。据我所知,框架保留了引用。每种表单类型一个。因此,如果您创建了100个Form1实例,然后将它们全部关闭,那么只有99个实例可以正确回收。我没有找到任何方法来治愈这个问题。
我们的应用程序已经迁移到了.NET2,这似乎要好得多。当我们打开第一个表单时,我们的应用程序内存仍然会增加,当它关闭时不会下降,但我相信这是由于JIT代码和加载的额外控制库造成的。

我还发现,尽管GC可以处理循环引用,但它似乎(有时)在循环事件处理程序引用方面存在问题。IE object1引用object2,object1有一个处理object2中的事件的方法。我发现这样的情况并没有在我期望的时候释放对象,但我从未能够在测试用例中重新生成它。

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

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

  • 对全局对象的引用
  • 对静态对象的引用
  • 对静态字段的引用
  • 堆栈上对本地对象的引用
  • 堆栈上对传递给方法的对象参数的引用
  • 对等待最终确定的对象的引用
  • CPU寄存器中对托管堆上对象的引用
  • 活动根目录列表由CLR维护。垃圾收集器的工作方式是查看托管堆上的对象,并查看应用程序仍然可以访问哪些对象,即通过应用程序根访问哪些对象。这样的对象被认为是根对象


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

    我想回应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;
        }
    
        // ...
    }