1. 程式人生 > >簡單說說.Net中的弱引用

簡單說說.Net中的弱引用

弱引用是什麼?

要搞清楚什麼是弱引用,我們需要先知道強引用是什麼。強引用並不是什麼深奧的概念,其實我們平時所使用的.Net引用就是強引用。例如:

Cat kitty = new Cat();

變數kitty就是一個強引用,它指向了堆中的一個Cat物件例項。我們都知道,CLR的垃圾回收機制會標記所有被強引用到的物件,而那些剩下的未被標記的物件則會被垃圾回收。換句話說,如果一個物件一直被某個強引用所指向,那麼它是不會被垃圾回收的。

從這一點來看,弱引用就完全不一樣了——即使某個物件被弱引用所指向,該物件仍然會被垃圾回收。也就是說,弱引用不會影響物件的生命週期。

System.WeakReference類是.net為我們提供的一個弱引用的實現,可以這麼用:

WeakReference weakReference = new WeakReference(new Cat());
Cat strongReference = weakReference.Target as Cat;
if (strongReference != null)
{
    // Cat物件例項尚未被垃圾回收,可以通過strongReference進行訪問
}
else
{
    // Cat物件例項已被垃圾回收
}

如果在上例的第一行程式碼之後第二行程式碼之前,CLR發生了一次垃圾回收,那麼可以基本斷定那個Cat物件例項已經不存在了,此時weakReference.Target是null。

WeakReference型別還有一個建構函式的過載為:

Public WeakReference(Object target, bool trackResurrection)

其中bool型別的引數trackResurrection指定了這個WeakReference例項是一個長弱引用還是一個短弱引用。對於短弱引用,當它所指向的物件被垃圾回收機制標記為“不可達”狀態(即將被回收)時,該弱引用的Target屬性即為null。而對於長弱引用,當它所指向物件的解構函式被呼叫之後,它的Target屬性仍然是有效的。

弱引用的內部實現

弱引用看起來很神奇,似乎是凌駕於正常的垃圾回收機制之上的,它究竟是如何實現的呢?其實WeakReference型別在內部封裝了一個名為GCHandle的struct型別,正是這個GCHandle使弱引用成為可能。

CLR中的每個AppDomain都擁有一個GC控制代碼表。這個表的每一項記錄有兩個資訊,一個是指向堆中某個物件的指標,另一個是這個表項的型別。總共有4種表項型別,其中Weak和WeakTrackResurrection兩種型別和我們今天所討論的弱引用相關。GCHandle這個類提供了一些操縱GC控制代碼表的方法。我們可以使用它的Alloc方法向GC控制代碼表中新增一個指定型別的表項。當垃圾回收開始後,垃圾回收器找到所有可達物件(簡單的說,就是有用的物件)。然後遍歷GC控制代碼表中每個Weak型別的表項,如果發現某表項所指的物件不屬於可達物件,則會把該表項的物件指標設定為null。緊接著,垃圾回收器會找出所有不可達物件中定義了解構函式的物件,並把他們放到一個被稱為freachable的佇列中(freachable中的物件會等待一個CLR中特定的執行緒來呼叫他們的終結函式)。由於這些freachable中的物件現在又被freachable佇列所引用,所以它們又成為可達物件了。此時,垃圾回收器會遍歷GC控制代碼表中所有WeakTrackResurrection型別的表項,和剛才一樣,如果某表項所指的物件不屬於可達物件,則會把該表項的物件指標設定為null。此處需注意,對於那些一開始被判定為不可達且定義了解構函式的物件來說,它們在GC控制代碼表中所對於的表項指標仍然不是null。這就是Weak和WeakTrackResurrection兩種型別的區別。

WeakReference就是通過表示了某個GC控制代碼表表項的GCHandle物件來完成跟蹤物件生命週期的功能的。你也一定可以看出短弱引用利用了Weak型別的GC控制代碼表項,而長弱引用則利用了WeakTrackResurrection型別的表項。

WeakReference的一些注意事項

首先,WeakReference自身也實現了解構函式。也就是說,它即使不再被使用了,也不會被立即回收,而是會在記憶體裡賴著多活一會(可能會經歷不止一次的垃圾回收)。

另外,如上一節所說,WeakReference會向GC控制代碼表新增一個表項。而每次垃圾回收,GC控制代碼表都會被遍歷一遍。可想而知,如果系統中存在大量的WeakReference,那麼GC控制代碼表很可能也會非常龐大,導致垃圾回收的效率降低。

WeakReference經常會和快取聯絡起來,但是它並不適和用來實現一個大型的快取機制。這是為什麼呢?一方面如前文所述,WeakReference自身實現了解構函式,也有可能導致垃圾回收的效率降低,因此應該避免在記憶體中建立大量的WeakReference物件例項。另一方面,我們知道一個物件如果沒有被任何強引用所指向,而僅僅被弱引用所指向,那麼它很有可能活不過一次垃圾回收。所以通過這樣的方式所實現出來的快取機制勢必有著非常短促的快取策略,而這種策略在大部分情況下都不會是你期望得到的。

WeakReference的三個使用場景

物件快取

試想這樣一個場景,我有一個記憶體受限的程式,在這個程式裡經常會使用一個佔用很多記憶體的點陣圖物件,所幸生成這個點陣圖物件並不複雜。所以我每次要使用那個點陣圖物件的時候都會重新生成它,使用完畢之後就將其丟棄(不保留它的引用)。

這種方式完全能夠滿足我的需求,但是還能不能再優化呢?分析一下我們就可以發現,當我需要使用點陣圖物件的時候,我上次使用的那個點陣圖物件雖然被我丟棄了,但可能仍然沒有被垃圾回收,仍然存在記憶體中。此時如果我能直接使用這個點陣圖物件,就可以節省出因重建點陣圖物件而浪費的記憶體和CPU資源。

改進措施很簡單——使用完點陣圖物件後,不是直接丟棄,而是用一個弱引用指向它。待下次訪問點陣圖物件時,就可以先通過弱引用判斷點陣圖物件是否還在記憶體中,如果還在則直接使用,否則重新建立。

 輔助除錯

有時,對於某種型別,我們需要知道當前程式中存在有多少物件例項,以及存在的都是哪些例項,以便於我們進行一些效能分析。這時,我們就可以使用到弱引用了。例如下面的程式碼: 

public class A
{
    private static List<WeakReference> _instances = new List<WeakReference>();

    public A()
    {
        _instances.Add(new WeakReference(this));
    } 

    public static int GetInstanceCount()
    {
        GC.Collect();
        return _instances.Count(x => x.Target != null);
    }
}

 GetInstanceCount方法可以得到記憶體中A型別的例項個數。另外,還可以通過instances集合來檢查記憶體中的A型別例項都有哪些。在除錯記憶體洩露問題的時候,這些資訊都可以派上用場。

相比於各種效能分析Profiler工具,這種方法更加輕巧便捷。 

弱事件

.Net中的事件有時會引起記憶體洩露問題。例如,A註冊了B的某個事件,此時B就會暗中保留A的一個強引用,導致A無法被記憶體回收,直到B被回收或A反註冊了B的事件。例如,我有一個物件註冊了主視窗的Loaded事件,只要我不反註冊該事件,那麼主視窗會一直引用該物件,直到主視窗被關閉,該物件才會被回收。所以,每當我們註冊某個物件的事件時,都有可能在不經意間埋下記憶體洩露的隱患。

解決這個問題的根本方法是,在必要的時候進行事件的反註冊。但是,在某些情況下,我們可能很難判定這個“必要的時候”。另外,當我們作為類庫的提供者時,我們也很難保證類庫的使用者都記得要反註冊事件。因此,另一個解決方案就是使用弱事件。

弱事件的實現原理很簡單,就是對事件進行一層封裝。不讓事件釋出者直接引用監聽者,而是讓他們保留一個監聽者的弱引用。當事件觸發時,釋出者會先檢查監聽者是否還存在於記憶體中,如果存在才通知它。如此一來,監聽者的生命週期就不會依賴於釋出者了。