C#內存泄漏--event內存泄漏

內存泄漏是指:當一塊內存被分配後,被丟棄,沒有任何實例指針指向這塊內存, 而且這塊內存不會被GC視爲垃圾進行回收。這塊內存會一直存在,直到程序退出。C#是託管型代碼,其內存的分配和釋放都是由CLR負責,當一塊內存沒有任何實例引用時,GC會負責將其回收。既然沒有任何實例引用的內存會被GC回收,那麼內存泄漏是如何發生的?測試

內存泄漏示例
  爲了演示內存泄漏是如何發生的,咱們來看一段代碼this

複製代碼
class Program 
{
    static event Action TestEvent;
    static void Main(string[] args)
    {
        var memory = new TestAction();
        TestEvent += memory.Run;
        OnTestEvent();
        memory = null;
        //強制垃圾回收
        GC.Collect(GC.MaxGeneration);
        Console.WriteLine("GC.Collect");
        //測試是否回收成功
        OnTestEvent();
        Console.ReadLine();
    }
    public static void OnTestEvent() {
        if (TestEvent != null) TestEvent();
        else Console.WriteLine("Test Event is null");
    }線程

    class TestAction 
    {
        public void Run() {
            Console.WriteLine("TestAction Run.");
        }
    }
}
複製代碼
   該例子中,memory.run訂閱了TestEvent事件,引起事件後,會在屏幕上看到 TestAction Run。當memory =null 後,memory原來指向的內存就沒有任何實例再引用該塊內存了,這樣的內存就是待回收的內存。GC.Collect(GC.MaxGeneration)語句會強制執行一次垃圾回收,再次引起事件,發現屏幕上仍是會顯示TestAction Run。該內存沒有被GC回收,這就是內純泄漏。這是由TestEvent+=memory.Run語句引發的,當GC.Collect執行的時候,當他看到該塊內存還有TestEvent引用,就不會進行回收。可是該內存已是「沒法到達」的了,即沒法調用該塊內存,只有在引起事件的時候,才能執行該內存的Run方法。這顯然不是我想要的效果,當memory = null執行時,我但願該內存在GC執行時被回收,而且當TestEvent被引起時,Run方法不會執行,由於我已經把該內存「解放」了。指針

  這裏有一個問題,就是C#中如何「釋放」一塊內存。像C和C++這樣的語言,內存的聲明和釋放都是開發人員負責的,一旦內存new了出來,就要delete,否則就會形成內存泄漏。這更靈活,也更麻煩,一不當心就會泄漏,忘記釋放、線程異常而沒有執行釋放的代碼...有手動分配內存的語言就有自動分配和釋放的語言。最開始使用垃圾回收的語言是LISP,以後被用在Java和C#等託管語言中。像C#,CLR負責內存的釋放,當程序執行一段時間後,CLR檢測到垃圾內存已經值得進行一次垃圾回收時,會執行垃圾回收。至於如何斷定一塊內存是否爲垃圾內存,比較著名的是計數法,即有一個實例引用了該內存後,就在該內存的計數上+1,改實例取消了對該內存的引用,計數就-1,當計數爲0時,就被斷定爲垃圾。該種方法的問題是對循環引用一籌莫展,如A的某個字段引用了B,而B的某個字段引用了A,這樣A和B的技術都不會降到0。CLR改用的方法是相似「標記引用法」(我本身的命名):在執行GC時,會掛起所有線程,並將託管堆中全部的內存都打上垃圾的標記,以後遍歷全部可到達的實例,這些實例若是引用了託管堆的內存,就將該內存的標記由垃圾變爲被引用。當遇到A和B相互引用的時候,若是沒有其餘實例引用A或者B,雖然A和B相互引用,可是A和B都是不可到達的,即沒辦法引用A或者B,則A和B都會被斷定爲垃圾而被回收。講解了這麼一大堆,目的就是要說,在C#中,你想要釋放一塊內存,你只要讓該塊內存沒有任何實例引用他,就能夠了。那麼當執行memory = null後,除了對TestEvent的訂閱,沒有任何實例再引用了該塊內存,那麼爲何訂閱事件會阻止內存的釋放?對象

  咱們來看看TestEvent+=memory.Run()這句話都幹了什麼。咱們利用IL反編譯上面的dll,能夠看到事件

複製代碼
1 IL_0000:  nop
2 IL_0001:  newobj     instance void EventLeakMemory.Program/TestAction::.ctor()
3 IL_0006:  stloc.0
4 IL_0007:  ldloc.0
5 IL_0008:  ldftn      instance void EventLeakMemory.Program/TestAction::Run()
6 IL_000e:  newobj     instance void [mscorlib]System.Action::.ctor(object, native int)
7 IL_0013:  call       void EventLeakMemory.Program::add_TestEvent(class [mscorlib]System.Action)
...//其餘部分
複製代碼
   關鍵在5-7行。第5和6行,聲明瞭一個System.Action型的委託,參數爲TestAction.Run方法,第七行,執行了Program.add_TestEvent方法,參數是上面聲明的委託。也就是說+=操做符至關於執行了Add_TestEvent(new Action(memory.Run)),就是這個new Action包含了對memory指向的內存的引用。而這個引用在CLR看來是可達的,能夠經過引起事件來調用該內存。內存

解決辦法
  咱們已經找到了內存泄漏的元兇,就是訂閱事件時,隱式聲明的匿名委託對內存的引用。該問題的解決辦法是使用一種和普通的引用不一樣的方式來引用方法的實例對象:該引用不會影響垃圾回收,不會在GC時被斷定爲對該內存的引用,也就是「弱引用」。C#中,絕大部分的類型都是強引用。如何實現弱引用?來看一個例子:開發

複製代碼
static void Main(string[] args){
    var obj = new object();
    var gcHandle = GCHandle.Alloc(obj, GCHandleType.Weak);
    Console.WriteLine("gcHandle.Target == null is :{0}", gcHandle.Target == null);
    obj = null;
    GC.Collect();
    Console.WriteLine("GC.Collect");
    Console.WriteLine("gcHandle.Target == null is :{0}", gcHandle.Target == null);
    Console.ReadLine();
}
複製代碼
  當執行GC。Collect後,gcHandle.Target == null 由false 變成了true。這個gcHandle就是obj的一個弱引用。這個類的詳細介紹見 GCHandle 。比較關鍵的是GCHandle.Alloc方法的第二個參數,該參數接受一個枚舉類型。我使用的是GCHandleType.Weak,代表該引用是個弱引用。利用這個方法,就能夠封裝一個本身的WeakReference類,代碼以下:rem

複製代碼
public class WeakReference<T> where T : class {
    private GCHandle handle;get

    public WeakReference(T obj) {
        if (obj == null) return;
        handle = GCHandle.Alloc(obj, GCHandleType.Weak);
    }

    /// <summary>
    /// 引用的目標是否還存活(沒有被GC回收)
    /// </summary>
    public bool IsAlive {
        get {
            if (handle == default(GCHandle)) return false;
            return handle.Target != null;
        }
    }

    /// <summary>
    /// 引用的目標
    /// </summary>
    public T Target {
        get {
            if (handle == default(GCHandle)) return null;
            return (T)www.furongpt.com handle.Target;
        }
    }
}
複製代碼
利用該類,就能夠寫一個本身的弱事件封裝器。

複製代碼
public class WeakEventManager<T> {
    private Dictionary<Delegate, WeakReference<T>> delegateDictionary;

    public WeakEventManager() {
        delegateDictionary = new www.taohuayuan178.com Dictionary<Delegate, WeakReference<T>>();
    }

    /// <summary>
    /// 訂閱
    /// </summary>
    public void AddHandler(Delegate handler) {
        if (handler != null)
            delegateDictionary[handler] = new WeakReference<T>(handler);
    }

    /// <summary>
    /// 取消訂閱
    /// </summary>
    public void RemoveHandler(Delegate handler) {
        if (handler != null)
            delegateDictionary.Remove(handler);
    }

    /// <summary>
    /// 引起事件
    /// </summary>
    public void Raise(object sender, EventArgs e) {
        foreach (var key in delegateDictionary.Keys) {
            if (delegateDictionary[key].IsAlive)
                key.DynamicInvoke(sender, e);
            else
                delegateDictionary.Remove(key);
        }
    }
}
複製代碼
最後,就能夠像下面這樣定義本身的事件了

複製代碼
public class TestEventClass {
    private WeakEventManager<Action<object, EventArgs>www.furggw.com/> _testEvent = new WeakEventManager<Action<object, EventArgs>>();
    public event Action<object, EventArgs> TestEvent {
        add { _testEvent.AddHandler(value);www.furggw.com/ }
        remove { _testEvent.RemoveHandler(value); }
    }

    protected virtual void OnEvent(EventArgs e) {         _testEvent.Raise(this, e);     }

相關文章
相關標籤/搜索