您知道吗:未释放事件Handler可能导致内存泄漏

以前曾看见过这样一个问题:托管代码会不会导致内存泄漏。自己对GC的了解也不是很深,但还是比较赞成这样的观点:托管代码不会产生内存泄漏,除非你没有正确释放非托管资源。
今天看到一个非常有趣的例子,关于没有释放事件的Handler导致的内存泄漏。
以前对于释放Handler的观念是一点也没有,这主要因为没此方面的意识,没有养成好的习惯。只知道当关心这个事件的时候就注册一下, 暂时不关心了就移除掉。却从来没有想到最终不移除不必要的Handler会导致此类无法被正常回收,导致不必要的内存浪费。

事情是这样的,今天在看项目Source Code的时候发现一个有趣的字眼:"WeakEvent". 自己以前对WeakReference有点了解,所以就好奇地看看这是个啥玩意。
发现其是一种通过弱引用实现的Delegate。因为没有太多的注释,所有不知其为啥用此种方式来封装事件。于是顺手Google了一下,找到了一篇关于weak event的非常有意思的文章。
文章里提出了一个问题,场景如下:

UnRelease Event Handler

运行的结果如下:

 image

虽然我们释放了对listener的引用,并且强制GC进行回收,但我们可以看到其内存占用量还是变大了,出乎了我的意料。
这就是该文作者指出的事件列表里保存的是一个强引用而非弱引用。虽然上面释放了listener变量对Listener实例的引用,但因为仍然在DisplaySettingsChanged事件列表里保存了对Listener实例的引用,导致Listener实例并不能被垃圾回收(有人引用,自然不会回收)。
那么接下来看看下面的代码:

Release Event Hanlder

运行结果如下:

image 
结果是不是正如您猜测的呢:)。已经成功地回收了listener实例。 不知为何从432944字节变到446980字节,哪位高手赐教一下啊:)
详情可以看原文    The Problem With Delegates
在后续的文章中作者类似文章开头提到的Weak Event来解决这个问题: Solving the Problem with Events: Weak Event Handlers

也许您觉得写这样的一个Weak Event没有必要或者显得麻烦,但您一定要记得及时地在必要的地方调用 -= 取消不再关心的事件。本文的目的也只是在此方面提个善意的提醒。

posted @ 2008-01-15 17:17 Anders06 阅读(2949) 评论(12) 编辑 收藏

 回复 引用 查看   
#1楼2008-01-15 17:35 | 蓝天旭日      
啊?handle都可以这样?
看来都注意咯!

 回复 引用   
#2楼2008-01-15 19:21 | bangbang[未注册用户]
因为SystemEvents.DisplaySettingsChanged是一个表态事件啦。
如果有一个静态类,假设它的名称为Test,有一DisplaySettingsListener(楼主代码中的那个类)类型的静态属性,假设为TestProperty,那我在DisplaySettingsListener构造函数里面代码改为Test.TestProperty = new DisplaySettingsListener();的话,GC还是无法回收那个类的实例的。
所以,这个时候handler和类的实例没有特殊之处。总之,就是在使用静态类的时候注意一下就可以了。

 回复 引用 查看   
#3楼2008-01-15 22:26 | yi      
SystemEvents.DisplaySettingsChanged

这个对象不可能垃圾,对于GC认为DisplaySettingsListener 不是垃圾

 回复 引用 查看   
#4楼[楼主]2008-01-16 09:25 | Anders06      
SystemEvents.DisplaySettingsChanged 只是用来举例子方便点。
想表达的意思是:事件列表里保存的是强引用, 如果handler不显示地从列表里移除, 那么只要这个Event Provider标记为垃圾后,Listener类才能被回收。
像上文的因为SystemEvents.DisplaySettingsChanged是静态的,所以Listener实例只有AppDomian卸载的时候才能被回收,相当于那块内存一直占着被浪费了,这叫强调了为什么显示撤销注册事件那么重要。当然你也可以用 weak Event,因为里面以被包装成弱引用

 回复 引用 查看   
#5楼2008-01-16 09:41 | Clark Zheng      
嗯,知道了,谢谢博主提醒
 回复 引用 查看   
#6楼2008-01-16 09:44 | yujiasw      
确实,类的静态成员是不会被回收的。因为可以认为它是全局的,永远不会成为垃圾。
 回复 引用   
#7楼2008-01-16 10:24 | .NET BUFF[未注册用户]
我来draw一个conclusion:
1.在CLR中值类型/引用类型中static变量被AppDomain中的Handle Table所mark, 并且为pinned类型, 因此GC并不负责回收之直到
AppDomain被unloaded.
2.Inside delegate我们可以发现, Delegate往往保存了对target(this指针)的reference
3.结合1,2, 由于static变量一直没被GC, 同时它又不负责任的hold了对this的引用, 最终导致了this所指的对象一直在memory中

对这一情况, tess有文如下:
NET Memory Leak Case Study: The Event Handlers That Made The Memory Baloon
http://blogs.msdn.com/tess/archive/2006/01/23/net-memory-leak-case-study-the-event-handlers-that-made-the-memory-baloon.aspx

 回复 引用 查看   
#8楼[楼主]2008-01-16 12:39 | Anders06      
谢谢 @.NET BUFF 的详细补充
 回复 引用 查看   
#9楼2008-01-19 00:11 | MS的明天      
还有这样的问题啊,谢谢楼主提供的信息.

 回复 引用 查看   
#10楼2008-09-05 16:40 | 颜昌钢      
比如有如下的代码:
public delegate void TestDelegate(string strTest);
public Class Test
{
public event TestDelegate TestDelegate;

protected void OnTestDelegate(string strTest)
{
if(TestDelegate!=null)
{
TestDelegate(strTest);
}
}

public void AddStr(string strTest)
{
OnTestDelegate(strTest);
}
}

public Class Test1
{
public void MethodTest()
{
Test test= new Test();
test.TestDelegate+=new TestDelegate(Test_TestDelegate);
test.AddStr("1");
}
void Test_TestDelegate(string strTest)
{
//ToDo:
}
}

请问,如上的怎么实现 test.TestDelegate-=new TestDelegate(Test_TestDelegate);

 回复 引用 查看   
#11楼2008-10-17 10:38 | onekey      
跟使用 SystemEvents.DisplaySettingsChanged有关系,调用的时候注意释放就可以了,代码可以这么处理:

class DisplaySettingsListener:IDisposable
{
byte[] m_ExtraMemory = new byte[1000000];

public DisplaySettingsListener()
{
SystemEvents.DisplaySettingsChanged += new EventHandler(ehDisplaySettingsChanged);
}

private void ehDisplaySettingsChanged(object sender, EventArgs e)
{
}

#region IDisposable Members

public void Dispose()
{
m_ExtraMemory = null;
}

#endregion
}

class Program
{
static void DisplayMemory()
{
Console.WriteLine("Total memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true));
}

static void Main()
{
DisplayMemory();
Console.WriteLine();
for (int i = 0; i < 5; i++)
{
Console.WriteLine("--- New Listener #{0} ---", i + 1);
using( DisplaySettingsListener listener = new DisplaySettingsListener())

GC.Collect();

DisplayMemory();

}
Console.Read();
}

}

 回复 引用 查看   
#12楼[楼主]2008-11-05 15:20 | Anders06      
@onekey
你上面的做法是释放了 m_ExtraMemory 内存,但没有释放DisplaySettingsListener 实例,虽然其可能只占用一点点内存。
本文的寓意不在于此,而是希望大家对事件能够引起注意,及时移除掉不需要的handler函数