來源:一路轉圈的雪人
其實吧,記憶體洩露一直是個令人頭疼的問題,在帶有GC的語言中這個情況得到了很大的好轉,但是仍然可能會有問題。
一、什麼是記憶體洩露(memory leak)?
記憶體洩露不是指記憶體壞了,也不是值記憶體沒插穩漏出來了,簡單來說,記憶體洩露就是在你期待的時間內你程式所佔用的記憶體沒有按照你想象中的那樣被釋放。
因此什麼是你期待的時間呢?明白這點很重要。如果一個物件佔用記憶體的時間和包含這個物件的程式一樣長,但是你並不期望是這樣。那麼就可以認為是記憶體洩露了。用具體例子來說明如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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方法來把外部的委託轉換成我們自己定義的委託。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
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壓力。我們可以通過強引用來表示物件池內最小的物件數量,通過弱引用來表示可以達到的最大的數量。