【问题标题】:Why this does not cause a memory leak when event is not unsubscribed为什么在未取消订阅事件时这不会导致内存泄漏
【发布时间】:2013-01-20 19:31:35
【问题描述】:

我试图了解事件如何导致内存泄漏。我在thisstackoverflow 问题上找到了一个很好的解释,但是在查看 Windg 中的对象时,我对结果感到困惑。首先,我有一个简单的类,如下所示。

class Person
    {
        public string LastName { get; set; }
        public string FirstName { get; set; }

        public event EventHandler UponWakingUp;
        public Person()  {  }

        public void Wakeup()
        {
            Console.WriteLine("Waking up");
            if (UponWakingUp != null)
                UponWakingUp(null, EventArgs.Empty);
        }
    }

现在我在一个 Windows 窗体应用程序中使用这个类,如下所示。

public partial class Form1 : Form
    {
        Person John = new Person() { LastName = "Doe", FirstName = "John" };

        public Form1()
        {
            InitializeComponent();

            John.UponWakingUp += new EventHandler(John_UponWakingUp);
        }

        void John_UponWakingUp(object sender, EventArgs e)
        {
            Console.WriteLine("John is waking up");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            John = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            MessageBox.Show("done");
         }
    }

如您所见,我实例化了 Person 类并订阅了 OnWakingUp 事件。我在这个表格上有一个按钮。当用户单击此按钮时,我将此 Person 实例设置为 null 而不取消订阅该事件。然后我调用 GC.Collect 以确保执行 Garbade 收集。我在这里显示一个消息框,以便我可以附加 Windbg 以查看 Form1 类的参考帮助,在此类中我没有看到对该事件的任何引用(Windbg 输出如下所示,尽管 Form1 的数据太长,我正在显示与我的问题有关)。这个类有一个对 Person 类的引用,但它是空的。基本上这对我来说似乎不是内存泄漏,因为 Form1 没有任何对 Person 类的引用,即使它没有取消订阅该事件。

我的问题是这是否会导致内存泄漏。如果不是,为什么不呢?

0:005> !do 0158d334   
Name:        WindowsFormsApplication1.Form1  
MethodTable: 00366390  
EEClass:     00361718  
Size:        332(0x14c) bytes  
File:        c:\Sandbox\\WindowsFormsApplication1\WindowsFormsApplication1\bin\Debug\WindowsFormsApplication1.exe  
Fields:  
      MT    Field   Offset                 Type VT     Attr    Value Name  
619af744  40001e0        4        System.Object  0 instance 00000000 __identity  
60fc6c58  40002c3        8 ...ponentModel.ISite  0 instance 00000000 site  
619af744  4001534      b80        System.Object  0   static 0158dad0 EVENT_MAXIMIZEDBOUNDSCHANGED  
**00366b70  4000001      13c ...plication1.Person  0 instance 00000000 John**  
60fc6c10  4000002      140 ...tModel.IContainer  0 instance 00000000 components  
6039aadc  4000003      144 ...dows.Forms.Button  0 instance 015ad06c button1  

0:008> !DumpHeap -mt 00366b70    
 Address       MT     Size  
total 0 objects  
Statistics:  
      MT    Count    TotalSize Class Name  
Total 0 objects  

【问题讨论】:

    标签: c# .net memory-leaks event-handling windbg


    【解决方案1】:

    这是一个循环引用的例子。该表单具有对通过 John 字段监听事件的对象的引用。反过来,当表单的构造函数订阅了它的 OnWakingUp 事件时,John 具有对表单的引用。

    循环引用在某些自动内存管理方案中可能是一个问题,尤其是在引用计数中。但是.NET 垃圾收集器没有问题。只要表单对象和 Person 对象都没有任何额外的引用,两者之间的循环引用就无法让对方保持活跃。

    在您的代码中没有额外的引用。这通常会导致两个对象都被垃圾收集。但是 Form 类是特殊的,只要它的本机 Windows 窗口存在,存储在 Winforms 维护的对象句柄表中的内部引用就可以使表单对象保持活动状态。这让约翰活着。

    所以通常的清理方式是用户通过点击右上角的 X 来关闭窗口。这反过来会导致本机窗口句柄被破坏。这会从该内部表中删除表单引用。下一次垃圾收集现在只看到循环引用并收集它们。

    【讨论】:

      【解决方案2】:

      答案实际上在您链接到的问题的答案中:

      当侦听器将事件侦听器附加到事件时,源 object 将获得对侦听器对象的引用。这意味着 listener 不能被垃圾收集器收集,直到 事件处理程序已分离,或源对象已收集

      您正在释放 source 对象 (Person),因此可以收集 Listener (您的 Form),这就是没有内存泄漏的原因.

      当这种情况与 I.E. 不同时,会发生内存泄漏。当您想要处置 Form 但事件 source(您的 Person 对象)仍然存在并持有对它的引用时。

      【讨论】:

      • 在这种情况下,Form1 是源类实例的侦听器。你是说 John 会有对 Form1 的引用????我现在完全糊涂了。
      • @palmsnow 是的,John.UponWakingUp 包含对包含对 Form1 的引用的委托的引用。否则无法引发该事件。
      • 请参阅this 文章了解更多详情。 BTW +1 对于一个结构良好的问题。
      • @svick:我明白这一点。该引用尚未清理,我认为这将是内存泄漏。如果你们中的任何人都可以添加一些可能更有帮助的示例,可能会是这样
      • @Benjamin。谢谢。因此,如果我有另一个表格(例如 Form2)。我在其中有一个用于 Form1 的类变量,然后处理 Form1 和调用 GC.Collect(),我是否仍然看到 Form1 没有在 Form2 中清理,因为在 Form1 中我们从未取消订阅此事件?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-02-29
      • 1970-01-01
      • 2022-01-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多