【问题标题】:How do I make sure a winform is garbage collected?如何确保 winform 被垃圾收集?
【发布时间】:2013-07-12 07:21:29
【问题描述】:

从网上和我的个人实验中了解到,GC 永远不会调用表单的终结器 (System.Windows.Forms.Form)。 据说在 Form GC.SuppressFinalize() 的 Dispose() 里面调用了 finalizer 就不会再被调用了。

例子:

public partial class UpdateForm : Form
{
    public UpdateForm()
    {
        InitializeComponent();

        // Listen to the event of some model
        Database.OnDataUpdated += new EventHandler(DataBase_OnDataUpdated);
    }

    ~UpdateForm()
    {
        // Never gets called.
    }

    private void DataBase_OnDataUpdated(object sender, EventArgs e)
    {
        // Update data on this form
    }
}

但是,如上例所示,如果表单连接 (+=) 某个模型的事件并且没有断开 (-=) Dispose() 中的事件,则表单永远不会被垃圾回收,即使Dispose() 被调用。

为了检查表单是否真的被垃圾回收,我在表单内创建了一个大数组以消耗大量内存,如下所示:

 int[] dummyArray = new int[1024 * 1024 * 128]; // Comsume 128MB memory

然后我在Windows中查看任务管理器的内存配置文件,看看我在处理表单后调用GC.Collect()时是否减少了内存使用量。

我的方法不聪明,我想知道是否有其他更聪明的方法或一些工具来确认表单实际上是垃圾收集?谢谢。

【问题讨论】:

  • 即使没有调用终结器,表单仍然会被垃圾收集。但是为什么要调用终结器呢?您可以在终结器中放入哪些不应该放入Dispose 方法的内容?
  • 抱歉造成混淆。我并不是说我想要调用终结器。我只是想确保表单被垃圾回收,即表单使用的所有内存都应该被释放。我通常使用终结器来检查一个对象是否真的被垃圾回收了,而这不能用表单来完成。

标签: c# winforms memory garbage-collection finalizer


【解决方案1】:
    Database.OnDataUpdated += new EventHandler(DataBase_OnDataUpdated);

是的,这是个问题。通用诊断是事件源对象比事件侦听器对象寿命长。或者换句话说,你的表单对象一直在监听数据库更新,即使它被用户关闭了。这通常会导致异常,这是您发现问题的最典型方式,当您的事件处理程序尝试更新已处置的控件时,样板文件是 ObjectDisposedException。目前尚不清楚您是如何设法避免这种故障模式的,请确保您没有使用 try/catch 等方式掩盖该故障模式。

而且,是的,它会导致 GC 问题。 Database 对象引用了您的表单对象。您在订阅活动时给了它参考。稍后在触发事件时再次使用它。必要,因为您的 DataBase_OnDataUpdated() 方法是您的类的实例方法。 C# 语法糖隐藏了这一事实。该简单事件赋值语句下的实际代码如下所示(不是有效的 C# 代码):

var delegateObject = new EventHandler(this, &DataBase_OnDataUpdated);
Database.OnDataUpdated = Delegate.Combine(DataBase_OnDataUpdated, delegateObject);

正是委托构造函数调用中隐藏的 this 将对表单对象的引用传递给数据库对象。它将它存储在 Delegate.Target 字段中。稍后在触发事件时使用。

因此,GC 不可避免地会看到对表单对象的引用,即使在它关闭之后也是如此。它在 Database 对象的委托调用列表中找到它。所以在数据库对象被垃圾收集之前,表单对象不能被垃圾收集。从您的问题来看,在您的程序终止之前不会发生这种情况。可能是因为它是一个静态变量。

还有其他模式可以避免这个问题。例如,您可以将对表单的引用传递给 Database 类,该类可以将其存储在正在侦听通知的活动表单列表中。它可以订阅表单的 Disposed 事件以了解表单已失效并从该列表中删除该对象。你还需要让你的表单实现一个接口,当有趣的事情发生时数据库类调用的方法。观察者模式的对立面,否则不如使用事件漂亮。

或者只是解决问题,因为您现在知道是什么原因造成的。只需明确地取消订阅事件:

    protected override void OnFormClosed(FormClosedEventArgs e) {
        Database.OnDataUpdated -= DataBase_OnDataUpdated;
        base.OnFormClosed(e);
    }

请注意,当您在 .NET 中订阅比其侦听器寿命更长的其他事件源时,如何需要完全相同类型的代码。就像 SystemEvents 和 Application.Idle 引发的事件一样

【讨论】:

  • 从我阅读问题的方式来看,OP 已经知道在哪里取消订阅该事件,并且只是在寻找一种方法来确认这确实解决了问题(没有其他参考阻止表单被垃圾收集)。事实上,像 OP 提到的那样,在 Dispose() 中取消订阅可能比在 OnFormClosed() 中更好,因为即使在显示表单之前抛出异常,通常也会调用 Dispose()。你已经提出了一个(非常)写得很好的答案,但我认为一个稍微不同的问题的答案。
  • @Hans:非常感谢您的详细而有用的回答。我确实从中学到了一些新东西。然而,hvd 的答案更接近我试图寻找的。抱歉,我可能无法清楚地解释我的问题。
【解决方案2】:

您可以使用WeakReference class 保持对表单的弱引用:

var weakref = new WeakReference(form);

弱引用不会阻止对象被垃圾回收,您可以使用该属性检查它是否已被垃圾回收:

if (weakref.IsAlive) { /* not yet garbage collected */ }

表单不需要终结器即可工作。

【讨论】:

  • 谢谢。这就是我一直在寻找的:一种检测对象(不一定是形式)是否还活着的通用方法。现在我想知道以表格为例可能会产生误导。
  • 我用上面的 WeakReference 做了一些实验。我观察到当没有其他对表单的引用时,即使在调用 GC.Collect() 之后,表单也不会被垃圾收集。我的猜测是 WeakReference 决定了表单是否可以被垃圾收集。因此,不能保证表单在调用 GC.Collect() 后立即被垃圾回收。
  • @Paul20 不,WeakReference 永远不会阻止对象(表单)被垃圾收集。如果您可以提出一个简短的示例,可靠地显示表单没有被垃圾收集,即使根据您的说法应该是,我很乐意看看。
  • 好的,这是一个老问题,顺便说一句,对于未来的读者来说,请确保您的表单没有代表仍然存在的其他表单/控件,因为当您注册到另一个表单/控件事件时,发布者(公开事件的控件)持有对被调用者的引用,因此您的表单仍然具有使他保持活动状态且不可收集的引用。为了解决这些问题,我建议使用 EventAggregator 并使用它公开/使用事件
猜你喜欢
  • 1970-01-01
  • 2015-02-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-21
  • 2013-10-18
相关资源
最近更新 更多