对象没有被垃圾回收

13

我认为这是一个C#初学者的问题,但是我似乎找不到正确的解决方案。

我有一个ClassOne对象,它定义了一个事件。我创建了一个ClassTwo对象,它被视为黑盒子,也就是说,我不知道它是否会注册任何事件。 ClassTwo的构造函数注册到ClassOne的事件。问题在于,当ClassTwo对象超出范围时,垃圾收集器从未删除此对象,因为它从未注销事件。

所以我有两个问题:

  1. 有没有一种方法让ClassTwo对象知道它何时超出范围? 对于旧的C++程序员来说,这将在析构函数中实现,但是在C#中,这种方式行不通。

  2. 是否有一种调试工具可以帮助我查找此类对象?

以下是重现此问题的示例代码:

    public partial class MainWindow : Window
{
    static public ClassOne classOne = new ClassOne();
    public MainWindow()
    {
        InitializeComponent();
        ClassTwo classtwo = new ClassTwo();
    }

    private void buttonTest_Click(object sender, RoutedEventArgs e)
    {
        GC.Collect();
    }
}
public class ClassOne
{
    public ClassOne()
    {
        Trace.WriteLine(this + " constructor");
    }

    ~ClassOne()
    {
        Trace.WriteLine(this + " destructor");
    }

    public delegate void UpdateFunc(object sender, EventArgs args);
    public event UpdateFunc OnUpdate;

}
public class ClassTwo
{
    public ClassTwo()
    {
        Trace.WriteLine(this + " constructor");
        MainWindow.classOne.OnUpdate += new ClassOne.UpdateFunc(classOne_OnUpdate);
    }

    void classOne_OnUpdate(object sender, EventArgs args)
    {
        throw new NotImplementedException();
    }

    ~ClassTwo()
    {
        Trace.WriteLine(this + " destructor");
    }
}

你是否正在寻求实现IDisposable - Brad Christie
2
+1 非常好的问题!事件比一开始看起来要棘手得多。 - user541686
@delnan:我认为楼主已经理解了这个概念,只是有一个轻微的命名问题(即需要适应)。这并不意味着楼主没有正确理解它。 - user541686
拥有语言级别的弱事件将是非常棒的。 - R. Martinho Fernandes
4
就翻译而言,第一版语言中的官方名称是“destructor”。由于造成了混淆,这个名称被更改了。 - R. Martinho Fernandes
显示剩余2条评论
3个回答

6
我会像这样在对象上实现 IDisposable 并在 Dispose 方法中取消注册事件。 您可以按照以下方式使用您的对象:
using(var two = new ClassTwo(classOne))
{
    // Do something with two
}
// object can now be garbage collected.

如果调用方未能调用Dispose,你就会很倒霉。

我认为我理解IDisposable的概念,但这取决于对象的使用者调用Dispose。 通常,一个对象应该自己清理东西,而不是依赖于调用者。 你是说没有办法做到这一点吗?那么我的第二个问题呢?调试器能帮助我找到那些从未被删除的对象吗? 作为一个初学者,我现在有一个相当大的程序,并意外发现我的一些对象没有被删除。 - MTR
@MTR:您可以使用内存分析器来检测此类对象。使用 IDisposable 是正确的方法 - 您的对象无法知道它不再需要了。只有调用者才能知道。 - Daniel Hilgarth
你能推荐一个好用且免费的内存分析器吗?你对这个解决方案有什么看法:http://blogs.msdn.com/b/greg_schechter/archive/2004/05/27/143605.aspx - MTR
我建议您只使用商业内存分析工具之一。它们都提供试用版本,例如JetBrains dotTrace。博客文章中讨论的解决方案在大多数情况下都过于复杂了,因为大多数情况下,实现IDisposable就足够了。如果由于某种原因这不适用于您,请使用博客文章中的解决方案。 - Daniel Hilgarth

5
  1. 只有实现了IDisposable接口并且调用方正确地调用Dispose方法,才能释放资源。(当然,为什么调用方不会合作呢?)

  2. 我没有听说过。:( 我认为你最好的选择是实现IDisposable接口,并在Dispose方法中注销。


0

正如其他人所提到的,Dispose模式是解决此问题的方法。如果ClassTwo对象的生命周期很短,您可以使用C# using语句确保在使用完对象后调用Dispose:

using (var foo = new ClassTwo())
{
    foo.Bar();
}

为了找到这些问题的根本原因,您需要使用内存分析器。dotTrace已经被提到过了,另一个好用的工具是SciTech Memory Profiler。您需要找到的是指向您认为应该被垃圾回收但实际上没有被回收的对象的根路径。根路径是导致对象无法被回收的原因,因为通过传递引用,一个保证存活的对象(GC Root)正在引用您想要清除的对象。
这些内存分析器非常有帮助,可以确定哪些GC根源是造成问题的原因,以及从根源到您的对象的引用路径。在那个根路径中,将会有一个不合适的引用,这就是问题的原因。
在您的代码中,ClassTwo对象无法被回收的原因可能是MainWindow对ClassOne对象有静态引用,或者从ClassOne到ClassTwo的事件处理程序没有被取消挂钩。静态引用是GC Roots的一个例子,因此,只要MainWindow中的静态引用没有更改,classOne引用的所有内容都将保持存活状态。

无论是静态引用还是事件处理程序,取决于您的应用程序场景 - ClassOne对象是否也应该被收集 - 是否对其进行静态引用是错误的?还是静态引用是期望的行为 - classOne是长期存在的对象,而classTwo是短期存在的对象,在这种情况下,当classTwo的生命周期结束时,应将其Dispose,并且Dispose应该取消挂钩事件处理程序。

这是一篇关于 .Net GC 的好文章,由 Jeffrey Richter 编写:http://msdn.microsoft.com/en-us/magazine/bb985010.aspx。虽然它有点过时了,但近年来GC已经有了新的补充,但这是一个很好的起点。


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