谈谈.NET中常见的内存泄露问题——GC、委托事件和弱引用
其实吧,内存泄露一直是个令人头疼的问题,在带有GC的语言中这个情况得到了很大的好转,但是仍然可能会有问题。
一、什么是内存泄露(memory leak)?
内存泄露不是指内存坏了,也不是值内存没插稳漏出来了,简单来说,内存泄露就是在你期待的时间内你程序所占用的内存没有按照你想象中的那样被释放。
因此什么是你期待的时间呢?明白这点很重要。如果一个对象占用内存的时间和包含这个对象的程序一样长,但是你并不期望是这样。那么就可以认为是内存泄露了。用具体例子来说明如下:
class Button { public void OnClick(object sender, EventArgs e) { ... } } class Program { static event EventHandler ButtonClick; static void Main(string[] args) { Button button = new Button(); ButtonClick += button.OnClick; } }
上面这段代码中,我们使用了一个静态的事件,而静态成员的生命周期是从AppDomain被加载开始,直到AppDomain被卸载,也就是说在通常情况下如果进程没被关闭,又忘记取消注册事件,那么ButtonClick事件包含的EventHandler委托所引用的对象会一直存在到进程结束为止,这就造成了内存泄露问题。这也是.NET中最常见的内存泄露问题的原因之一。后面我会接着说怎么解决这种事件造成的泄露问题。
二、内存回收的方式
1、引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个 变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办 法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。
像原来IE6中Javascript中原生对象内存回收的方式就是通过检查对象是否有引用来判断一个对象是否是垃圾。IE9之前,其BOM和DOM中的对象是使用C++以COM对象的形式实现的,而COM对象的垃圾收集机制采用的也是引用计数策略。而这种方式通常会因为循环引用导致内存泄露,也就是A引用B的同时,B也引用者A。 在Object C中也会有这样的循环引用的问题。在Object C中的解决方案就是给一方标记为weak,介绍可以参看这里, 关于Object C中的委托模式的介绍。
2、标记清除法(mark-weep)
C#中采用的是标记法回收内存,全部对象都要标记,并且只标记一次就不再标记。判断一个对象是不是垃圾取决于是否有引用,而是取决是是否被root引用。
root的类型有寄存器中的变量,线程栈上的变量,静态变量等。
我们来看一幅通常情况下的对象图,图中有一个循环引用。
我们抽取其中一部分图说明
在采用标记清除策略的实现中,由于函数执行之后,local3出栈,离开了作用域,因此这种相互引用在标记清除法中不是个问题。
我们很容易看出,因为每一个对象都要mark,因此创建大量的小对象会给Mark阶段造成压力。值得注意的是,在GC的mark 和weep阶段,会挂起所有线程,因此创建大量的线程也是会对GC造成问题。这个问题我以后会再讨论。
三、弱引用解决一些问题
如前面所说,忘记取消注册事件通常是.NET中最常见的内存泄露问题,我们怎么自动化的解决这个问题呢?也就是说当方法所属的对象已经被标记为垃圾的时候,我们就在事件中取消注册这个方法。这时就可以通过弱引用来实现。
委托的本质就是一个类,包含了几个关键属性
1.指向原对象的Target属性(强引用)。
2.一个指向方法的ptr指针。
3.内部维护着一个集合(delegate是以链表结构实现)
因为.NET中的委托是强引用,我们要把它改成弱引用,我们可以抓住这个这些特征,创建一个自己的WeakDelegate类。
事件的本质就是一个访问器方法,和委托的关系类似于字段和属性,也就是控制外部对字段的访问。我们可以通过自定义add和remove方法来把外部的委托转换成我们自己定义的委托。
public class Button { private class WeakDelegate { public WeakReference Target; public MethodInfo Method; }
private List<WeakDelegate> clickSubscribers = new List<WeakDelegate>();
public event EventHandler Click { add { clickSubscribers.Add(new WeakDelegate { Target = new WeakReference(value.Target), Method = value.Method }); } remove
{ ..... } }
public void FireClick() { List<WeakDelegate> toRemove = new List<WeakDelegate>(); foreach (WeakDelegate subscriber in clickSubscribers) {
//第一个Target表示方法所属的对象,第二个Target表示这个对象是否被标记为垃圾,如果为null则表示为已经被标记为垃圾。 object target = subscriber.Target.Target; if (target == null) { toRemove.Add(subscriber); } else { subscriber.Method.Invoke(target, new object[] { this, EventArgs.Empty }); } } clickSubscribers.RemoveAll(toRemove); } }
弱引用还可以用来创建一个对象池,对象池就是通过管理少量的对象来减少内存和GC压力。我们可以通过强引用来表示对象池内最小的对象数量,通过弱引用来表示可以达到的最大的数量。