为什么在未取消订阅事件时不会导致内存泄漏

pal*_*now 5 .net c# memory-leaks windbg event-handling

我试图了解事件如何导致内存泄漏.我在这个 stackoverflow问题上找到了很好的解释,但是在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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

现在我在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");
         }
    }
Run Code Online (Sandbox Code Playgroud)

如您所见,我实现了Person类并订阅了UponWakingUp事件.我在这张表格上有一个按钮.当用户单击此按钮时,我将此Person实例设置为null,而不取消订阅该事件.然后我调用GC.Collect以确保已执行Garbade集合.我在这里显示一个消息框,以便我可以附加Windbg来查看Form1类的引用帮助,并且在这个类中我没有看到任何对该事件的引用(尽管Form1的数据太长,我显示的Windbg输出如下所示与我的问题有关).此类具有对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  
Run Code Online (Sandbox Code Playgroud)

Han*_*ant 5

这是循环引用的情况.表单引用了通过John字段侦听事件的对象.反过来,John在表单的构造函数订阅了UponWakingUp事件时引用了表单.

循环引用可能是某些自动存储器管理方案中的问题,特别是在引用计数中.但.NET垃圾收集器没有问题.只要表单对象和Person对象都没有任何其他引用,两者之间的循环引用就不能保持对方的活动.

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

因此,清理它的正常方法是用户通过单击右上角的X来关闭窗口.这反过来导致本机窗口句柄被破坏.这将从该内部表中删除表单引用.下一个垃圾收集现在只看到循环引用,并收集它们.