.NET中常見的內存泄露問題——GC、委託事件和弱引用

1、什麼是內存泄露(memory leak)?

內存泄露不是指內存壞了,也不是指內存沒插穩漏出來了,簡單來講,內存泄露就是在你期待的時間內你程序所佔用的內存沒有按照你想象中的那樣被釋放。html

所以什麼是你期待的時間呢?明白這點很重要。若是一個對象佔用內存的時間和包含這個對象的程序同樣長,可是你並不指望是這樣。那麼就能夠認爲是內存泄露了。用具體例子來講明以下:函數

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中最多見的內存泄露問題的緣由之一。後面我會接着說怎麼解決這種事件形成的泄露問題。this

2、內存回收的方式

一、引用計數線程

引用計數的含義是跟蹤記錄每一個值被引用的次數。當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是1。若是同一個值又被賦給另外一個 變量,則該值的引用次數加1。相反,若是包含對這個值引用的變量又取得了另一個值,則這個值的引用次數減1。當這個值的引用次數變成0時,則說明沒有辦 法再訪問這個值了,於是就能夠將其佔用的內存空間回收回來。這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數爲零的值所佔用的內存。指針

像原來IE6中Javascript中原生對象內存回收的方式就是經過檢查對象是否有引用來判斷一個對象是不是垃圾。IE9以前,其BOM和DOM中的對象是使用C++以COM對象的形式實現的,而COM對象的垃圾收集機制採用的也是引用計數策略。而這種方式一般會由於循環引用致使內存泄露,也就是A引用B的同時,B也引用者A。 在Objective-C中也會有這樣的循環引用的問題。在Objective-C中的解決方案就是給一方標記爲weak,介紹能夠參看這裏,關於Objective-C中的委託模式的介紹。code

二、標記清除法(mark-weep)htm

C#中採用的是標記法回收內存,所有對象都要標記,而且只標記一次就再也不標記。判斷一個對象是否是垃圾取決因而否有引用,而是取決是是否被root引用。對象

root的類型有寄存器中的變量,線程棧上的變量,靜態變量等。blog

咱們來看一幅一般狀況下的對象圖,圖中有一個循環引用。生命週期

咱們抽取其中一部分圖說明

在採用標記清除策略的實現中,因爲函數執行以後,local3出棧,離開了做用域,所以這種相互引用在標記清除法中不是個問題。


咱們很容易看出,由於每個對象都要mark,所以建立大量的小對象會給Mark階段形成壓力。值得注意的是,在GC的mark和weep階段,會掛起全部線程,所以建立大量的線程也是會對GC形成問題。這個問題我之後會再討論。

3、弱引用解決一些問題

如前面所說,忘記取消註冊事件一般是.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壓力。咱們能夠經過強引用來表示對象池內最小的對象數量,經過弱引用來表示能夠達到的最大的數量。

相關文章
相關標籤/搜索