民初思韻網

加入收藏   設為首頁
選擇語言   簡體中文
你好,請 登陸 或 注冊
首頁 人文思韻 傳奇人物 歷史思潮 時代作品 話題討論 國民思韻 民初捐助 賬戶管理
  搜索  
    人文精神 >>> 技術的天空 溫和的思緒
字體    

談談.NET中常見的內存泄露問題——GC、委托事件和弱引用
談談.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

歡迎訂閱我們的微信公眾賬號!
春秋茶館訂閱號
微信號 season-tea(春秋茶館)
每天分享一篇科技/遊戲/人文類的資訊,點綴生活,啟迪思想,探討古典韻味。
  清末民初歷史人物  民初人物
教育專家大學思想啟蒙
蔡元培(1868年1月11日-1940年3月5日),字鶴卿,又字仲申、民友、孑民,乳名阿培,並曾化名蔡振、周子餘,浙江紹興山陰縣(今紹興縣)人,革命家、教育家、政治家。中....
新與古典文化研究大家
胡適(1891年12月17日-1962年2月24日),原名嗣穈,學名洪騂,字希疆,後改名胡適,字適之,筆名天風、藏暉等,其中,適與適之之名與字,乃取自當時盛行的達爾文學說....
資助民初精神網
        回頂部     寫評論

 
評論集
暫無評論!
發表評論歡迎你的評論
昵稱:     登陸  註冊
主頁:  
郵箱:  (僅管理員可見)

驗證:   验证码(不區分大小寫)  
© 2011   民初思韻網-清末民初傳奇時代的發現與復興   版權所有   加入收藏    設為首頁    聯繫我們    1616導航