为什么当事件未被取消订阅时,这不会导致内存泄漏

5

我正在尝试理解事件如何导致内存泄漏。我在这个stackoverflow问题中找到了一个很好的解释,但是当我查看Windbg中的对象时,结果让我感到困惑。首先,我有一个简单的类如下。

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类并订阅了UponWakingUp事件。在这个表单上我有一个按钮。当用户点击此按钮时,我将此Person实例设置为null而没有取消订阅该事件。然后我调用GC.Collect以确保垃圾回收已完成。我在这里显示一个消息框,以便我可以附加Windbg来查找Form1类帮助和引用。在这个类中,我没有看到该事件的任何引用(Windbg输出如下,尽管Form1数据过长,我仅展示与我的问题相关的部分)。该类对Person类有引用,但它是null的。基本上,在我的看法中,这似乎不像是内存泄漏,因为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  
2个回答

6
这是一个 "循环引用" 的案例。该表单通过 "John" 字段引用侦听事件的对象。而 John 在表单构造函数订阅其 UponWakingUp 事件时又引用了表单,因此形成了循环引用。
在某些自动内存管理方案中,循环引用可能会导致问题,特别是在引用计数中更为明显。但.NET垃圾回收器并不会有问题。只要表单对象和Person对象都没有其他引用,两者之间的循环引用就不能使彼此保持活动状态。
在您的代码中,两者都没有其他引用。这通常会导致两个对象被垃圾收集。但表单类很特殊,只要它有一个本地Windows窗口,Winforms维护的句柄到对象表中的内部引用就会使表单对象保持活动状态。这将保持 John 的活动状态。
因此,正常清除的方式是用户通过单击右上角的 X 关闭窗口。然后本机窗口句柄被销毁,从而将表单引用从该内部表中删除。下一次垃圾回收现在只会看到循环引用,并将同时收集它们。

4
实际上,答案已经在您所链接的问题的答案中了:
当监听器将事件监听器附加到事件时,源对象将获得对监听器对象的引用。这意味着,只要事件处理程序被分离或源对象被收集监听器就无法被垃圾回收器回收。
因此,您正在释放对象(Person),因此Listener(您的Form)可以被回收,这就是为什么没有内存泄漏的原因。
当情况相反时,即您想要处理Form,但事件(您的Person对象)仍然存活并持有对其的引用时,就会发生内存泄漏。

在这种情况下,Form1是源的侦听器,该源是Person类实例。你是说John会有一个对Form1的引用吗????我现在完全困惑了。 - palm snow
@palmsnow 是的,John.UponWakingUp 包含对一个委托的引用,该委托包含对 Form1 的引用。否则无法触发该事件。 - svick
@svick:我明白了。那个引用没有被清理,我以为那可能是内存泄漏。如果你们中的任何人能够添加一些示例,那可能会更有帮助。 - palm snow
@Benjamin。谢谢。所以如果我有另一个窗体(比如Form2)。我在其中有一个类变量用于Form1,然后处理掉Form1并调用GC.Collect(),那么我是否仍然应该在Form2中看到未清除的Form1,因为在Form1中我们从未取消订阅此事件? - palm snow
你能在你的回答中添加一些示例代码,导致内存泄漏吗? - palm snow
显示剩余3条评论

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