33

我想在Windows窗体应用程序中运行内存泄漏。我现在正在查看一个包含多个嵌入表单的表单。让我担心的是,孩子在他们的构造函数中形成了父表单的引用,并将其保存在私有成员字段中。因此,在我看来垃圾收集时间:循环引用导致内存泄漏?

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

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

这是对垃圾收集器如何评估情况的准确理解?任何方式来“证明”它的测试目的?

回答

36

好问题!

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

如果在上述过程中找到的“根”引用对象之一的属性中引用了所有其他引用变量(或GC'd)...(或在根对象中的引用引用的对象中,等等......)

所以只有当其中一个表单被引用到“根”引用的其他地方时 - 那么这两个表单对于GC来说都是安全的。

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

+2

或者只是在每个表单中分配一个大量缓冲区。 – Gusdor 2012-02-06 12:22:27

5

如果家长和孩子都没有被引用,但他们只引用彼此,他们确实得到GCed。

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

0

GC可以正确处理循环引用,如果这些引用是保持表单活着的唯一东西,那么它们将被收集。
我有很多麻烦,.NET不从窗体回收内存。在1.1版本中,有一些错误发生在menuitem(我认为)上,这意味着它们没有被处置并且可能会泄漏内存。在这种情况下,添加显式调用来处理和清除表单的Dispose方法中的成员变量对问题进行排序。我们发现这也有助于回收一些其他控制类型的内存。
我也花了很长时间与CLR分析器查看为什么表单没有被收集。据我所知,框架保留了参考文献。每个表单类型一个。因此,如果您创建100个Form1实例,然后关闭它们,只有99个会被正确回收。我没有找到任何方法来解决这个问题。
我们的应用程序已经转移到.net 2,这似乎更好。我们的应用程序内存在我们打开第一个窗体时仍然增加,并且在关闭时不会退出,但我相信这是由于JIT的代码和额外的控制库被加载。
我也发现虽然GC可以处理循环引用,但它似乎有问题(有时)与循环事件处理程序引用。IE对象1引用对象2和对象1有一个方法来处理和来自对象2的事件。我发现当我预期的时候这并没有释放对象的情况,但我从来没有能够在测试用例中重新生成它。

16

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

+2

描述你的例子的答案:http://stackoverflow.com/a/12133788/6345 – 2012-10-23 21:22:08

12

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

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

活动根目录由CLR维护。垃圾收集器通过查看托管堆上的对象并查看应用程序仍可访问哪些对象来工作,即可通过应用程序根访问。这样的对象被认为是根植的。

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

+0

@Jason,你是什么意思的“对象参数”? 我相信引用的位置是关键的决定因素......如果在堆栈中,或者类的静态成员或CPU寄存器中,那么它就是一个根引用。 ... 否则不是。 (除了freachable队列, - 另一个话题) – 2008-12-30 17:12:32

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; 
    } 

    // ... 
}