要搞清楚什麼是弱引用,咱們須要先知道強引用是什麼。強引用並非什麼深奧的概念,其實咱們平時所使用的.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類型還有一個構造函數的重載爲:this
Public WeakReference(Object target, bool trackResurrection)
其中bool類型的參數trackResurrection指定了這個WeakReference實例是一個長弱引用仍是一個短弱引用。對於短弱引用,當它所指向的對象被垃圾回收機制標記爲「不可達」狀態(即將被回收)時,該弱引用的Target屬性即爲null。而對於長弱引用,當它所指向對象的析構函數被調用以後,它的Target屬性仍然是有效的。spa
弱引用看起來很神奇,彷佛是凌駕於正常的垃圾回收機制之上的,它到底是如何實現的呢?其實WeakReference類型在內部封裝了一個名爲GCHandle的struct類型,正是這個GCHandle使弱引用成爲可能。.net
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會向GC句柄表添加一個表項。而每次垃圾回收,GC句柄表都會被遍歷一遍。可想而知,若是系統中存在大量的WeakReference,那麼GC句柄表極可能也會很是龐大,致使垃圾回收的效率下降。
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事件,只要我不反註冊該事件,那麼主窗口會一直引用該對象,直到主窗口被關閉,該對象纔會被回收。因此,每當咱們註冊某個對象的事件時,都有可能在不經意間埋下內存泄露的隱患。
解決這個問題的根本方法是,在必要的時候進行事件的反註冊。可是,在某些狀況下,咱們可能很難斷定這個「必要的時候」。另外,當咱們做爲類庫的提供者時,咱們也很難保證類庫的使用者都記得要反註冊事件。所以,另外一個解決方案就是使用弱事件。
弱事件的實現原理很簡單,就是對事件進行一層封裝。不讓事件發佈者直接引用監聽者,而是讓他們保留一個監聽者的弱引用。當事件觸發時,發佈者會先檢查監聽者是否還存在於內存中,若是存在才通知它。如此一來,監聽者的生命週期就不會依賴於發佈者了。