談談.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。 在Objective-C中也會有這樣的循環引用的問題。在Objective-C中的解決方案就是給一方標記為weak,介紹可以參看這里,關于Objective-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壓力。我們可以通過強引用來表示對象池內最小的對象數量,通過弱引用來表示可以達到的最大的數量。


一路轉圈的雪人 2013-08-31 16:51:55

[新一篇] 紀錄片:與霍金一起了解宇宙

[舊一篇] 使用緩存的9大誤區
回頂部
寫評論


評論集


暫無評論。

稱謂:

内容:

驗證:


返回列表