GC会释放已使用的内存吗?

9

我有一个父对象,它引用了一个子对象,并且父对象还有一个事件处理程序来监听子对象的事件。

如果所有对父对象的引用都被释放,那么通过父对象和子对象占用的内存会在GC中被释放吗?(假设既没有对子对象也没有对父对象的引用存在)。

class ParentClass {

    ChildClass _childClass;

    public ParentClass(ChildClass childClass) {
        _childClass = childClass;
        childClass.SomeEvent += ChildClass_SomeEvent;
    }

    void ChildClass_SomeEvent(object sender, SomeEventArgs e) {

    }
}

请注意,我知道GC不会立即作出反应。我的问题不是在释放父对象后内存是否立即被释放,而是内存是否被释放。更新:对于我来说,答案很明显,是的,GC能够解决这个循环引用。但是对于所有阅读此帖子并有类似问题的人,请小心不要将事件注册保持打开。它只是一个特殊的例子,在其中注册不是问题。在其他情况下,事件注册可能会导致严重的内存泄漏。vilx提供了一个非常好的涵盖此问题的资源:http://www.interact-sw.co.uk/iangblog/2004/07/07/circulareventrefs
4个回答

9
是的,.NET GC 可以处理循环引用问题(假设您没有使用非托管资源或者如果您使用了 IDisposable) 。

GC可以像你所说的那样处理任何引用情况,但我认为你需要澄清OP示例中事件注册的使用——在这种情况下会发生什么?在.NET中,任何剩余注册的事件处理程序都可能成为潜在的内存泄漏源(内存泄漏意味着GC无法收集)。 - Adam Houldsworth
这是我长期以来的一个问题。它并不特定于事件,我选择事件作为例子,因为我的英语很糟糕,无法解释真正的问题,以便有人能够理解并回答。它涉及到两个对象之间的循环引用,它们彼此相关但没有进一步的引用。讨论已经进入了这个话题。 - HCL
我建议您修改您的OP以删除事件使用 - 因为事件引入了一层复杂性,这是您的问题中无法忽略的。无论如何,好问题 :-) - Adam Houldsworth
1
你需要实现IDisposable接口,并手动调用dispose()方法。 - mishal153
1
事件的问题在于,许多开发人员不知道(或忽略)事件会从源到监听器创建强引用。如果您了解这一点,可以实施对策(如弱事件模式、取消订阅等)。 - Jaroslav Jandek
显示剩余2条评论

3

如果没有其他内容使用相同的 _childClass 实例,那么它将被回收。 但是,我不确定您未分离事件处理程序的影响是什么 - 在.NET应用程序中,注册的事件处理程序是无法垃圾回收的内存泄漏的来源 - 您对问题的看法很有趣。

更新:结果发现我对内存泄漏源的理解是错误的。 如果A类订阅了B类但没有取消订阅,则只有在B被收集时才会收集A(因为B的订阅列表使得A活着)。 对于长期存在的事件源,这可能会成为订阅者的问题。

这个问题发生在所有对象的生命周期中,但实际上,如果B的寿命和A一样短暂,那么泄漏不会被注意到,也不通常是一个问题。

更新2:在OP示例中,子项是源,父项是订阅者 - GC可以处理这种情况 - 只要子项在父项之外没有引用,这意味着子项是不可回收的(这将保持父项的活着)。


3
依我之见,事件处理程序和委托一般没有任何不同,它们只是其他引用的一种形式。因此,不需要显式分离。事件处理程序往往是“内存泄漏”的罪魁祸首,因为人们往往会忘记它们的存在,但仅此而已。 - Vilx-
如果你从类A向类B注册一个事件,然后A被回收了,由类A注册的事件处理程序的死引用会导致你无法回收B - 这是.NET中泄漏的根源。如果在终结器或其他方式中A移除订阅,然后再进行回收,那么B也可以正常回收。 - Adam Houldsworth
据我所知,事件委托包含弱引用,因此不会阻止类被垃圾回收。在其他环境中可能是内存泄漏的源头,但我还没有遇到过这种情况。 - Noldorin
1
你把某些东西搞混了。“订阅列表”与任何其他集合(比如标准的ArrayList)没有任何区别,它是具有事件的类的成员。因此,如果对该类的所有引用都丢失了,那么对“订阅列表”的引用也将丢失,两个对象将一起被收集。此外,使用您的关键字进行谷歌搜索,我找到了这篇文章,完全支持我上面所说的:http://www.interact-sw.co.uk/iangblog/2004/07/07/circulareventrefs - Vilx-
3
有一个常见的泄漏模式是这样的:A.e += B.m; B = null; GC.Collect(); 这个时候人们经常会想知道为什么 B 没有被回收。这是一个典型的情况,很容易忽略。我也曾遇到过这样的 bug(找出来还挺“有趣”的……)但反过来却行不通,那只是一种谣言,由那些真正不理解这一切的人散布开来的。 - Vilx-
显示剩余15条评论

1

当堆上的某个特定对象没有更多强引用时(即引用计数已降至零),GC确实会安排释放该对象的内存。这些引用的定义位置和方式并不重要;它们由GC跟踪并相应地处理。

然而值得注意的是:GC本质上是非确定性的方式(首先它在单独的线程上运行),可能需要一些时间才能销毁内存中的对象。 "一些时间"通常几乎是瞬间完成,除非处理器过于繁忙和/或一次释放了大量内存等不寻常情况。


正如Joralsav Jandek所写的那样,GC意识到循环引用并不将子对象视为引用计数。 - HCL
在你提供的例子中,我认为并没有循环引用... 即使有,据我所知,GC也不必特别处理循环引用。每个对象的引用计数是完全足够的。 - Noldorin
父级引用了子级,在子级的事件列表中有一个对父级的引用,以通知它关于SomeEvent。这似乎是循环的。我错了吗? - HCL
哦,抱歉,我忽略了事件处理程序的定义位置。在这方面它是循环的,尽管如我所说,并没有真正产生影响。请查看我的评论,谢谢。 - Noldorin
循环引用对于基于引用计数的垃圾回收机制来说是一个问题。例如,VB6使用基于计数的垃圾回收机制,如果要防止内存泄漏,就必须清理循环引用。Python有特殊处理循环引用的方法。但是,这对于.NET的垃圾回收机制并不相关,因为它不是基于引用计数的,而是在垃圾回收过程中基于“可达性”进行跟踪。 - Nate C-K

1
childClass.SomeEvent += ChildClass_SomeEvent;

这段代码意味着子类引用了父类。这是一个循环引用,GC可以处理。在另一种情况下,取消订阅事件可能会很危险。假设你有一个独立的实例而不是childClass:

anotherClass.SomeEvent += AnotherClass_SomeEvent;

现在,当你认为ParentClass可能被收集时,如果anotherClass仍然存在且事件订阅未被取消,则它实际上仍然存活。只有当anotherClass被收集时,ParentClass才能被收集。


是的,我了解你所描述的情况。 - HCL

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