天天看點

了解弱引用(Understanding Weak References)轉

轉載:http://blog.csdn.net/xtyyumi301/archive/2008/10/04/3015493.aspx

Understanding Weak References

以前我招聘過進階java工程師,其中一個面試題目是“你對weak reference了解多少?”。這個話題比較偏,不指望每個人都能清楚它的細節。如果面試的人說“Umm...好像和gc(垃圾回收)有點關系?”,那我就相當滿意了。實際情況卻是20多個5年java開發經驗的工程師隻有2個知道有weak reference這麼回事,其中1個是真正清楚的。我試圖給他們一些提示,期望有人會恍然大悟,可惜沒有。不知道為什麼這個特性uncommon,确切地說,是相當uncommon,要知道這是在java1.2中推出的,那是7年前的事了。

沒必要成為weak reference專家,裝成資深java工程師(就像茴香豆的茴字有四種寫法)。但是至少要了解一點點,知道是怎麼回事。下面告訴你什麼是weak references,怎麼用及何時用它們。

l Strong references

從強引用(Strong references)開始。你每天用的就是strong reference,比如下面的代碼:StringBuffer buffer = new StringBuffer();建立了一個StringBuffer對象,變量buffer儲存對它的引用。這太小兒科了!是的,請保持點耐心。Strong reference,是什麼使它們‘strong’?——是gc處理它們的方式:如果一個對象通過一串強引用鍊可達,那麼它們不會被垃圾回收。你總不會喜歡gc把你正在用的對象回收掉吧。

l When strong references are too strong

我們有時候用到一些不能修改也不能擴充的類,比如final class,再比如,通過Factory建立的對象,隻有接口,連是什麼實作都不知道。想象一下,你正在用widget類,需要知道每個執行個體的擴充資訊,比如它是第幾個被建立的widget執行個體(即序列号),假設條件不允許在類中添加方法,widget類自己也沒有這樣的序列号,你準備怎麼辦?用HashMap!serialNumberMap.put(widget, widgetSerialNumber),用變量記錄新執行個體的序列号,建立執行個體時把執行個體和它的序列号放到HashMap中。很顯然,這個Map會不斷變大,進而造成記憶體洩漏。你要說,不要緊,在不用某個執行個體時就從map中删除它。是的,這可行,但是“put——remove”,你不覺得你在做與記憶體管理“new——delete”類似的事嗎?像所有自己管理記憶體的語言一樣,你不能有遺漏。這不是java風格。

另一個很普遍的問題是緩存,特别是很耗記憶體的那種,比如圖檔緩存。想象一下,有個項目要管理使用者自己提供的圖檔,比如像我正在做的網站編輯器。自然地你會把這些圖檔緩存起來,因為每次從磁盤讀取會很耗時,而且可以避免在記憶體中一張圖檔出現多份。你應該能夠很快地意識到這有記憶體危機:由于圖檔占用的記憶體沒法被回收,記憶體遲早要用完。把一部分圖檔從緩存中删除放到磁盤上去!——這涉及到什麼時候删除、哪些圖檔要删除的問題。和widget類一樣,不是嗎,你在做記憶體管理的工作。

l Weak reference

Weak reference,簡單地說就是這個引用不會強到迫使對象必須保持在記憶體中。Gc不會碰Strong reference可達的對象,但可以碰weak reference可達的對象。下面建立一個weak reference:WeakReference weakWidget = new WeakReference(widget),使用weakWidget.get()來取到widget對象。注意,get()可能傳回null。什麼?null?什麼時候變成null了?——當記憶體不足垃圾回收器把widget回收了時(如果是Strong reference,這是不可能發生的)。你會問,變成null之後要想再得到widget怎麼辦?答案是沒有辦法,你得重新建立widget對象,對cache系統這很容易做到,比如圖檔緩存,從磁盤載入圖檔即可(記憶體中的每份圖檔要在磁盤上儲存一份)。

像上面的“widget序列号”問題,最簡單的是用jdk内含的WeakHashMap類。WeakHashMap和HashMap的工作方式類似,不過它的keys(注意不是values)都是weak reference。如果WeakHashMap中的一個key被垃圾回收了,那麼這個entry會被自動删除。如果使用的是Map接口,那麼執行個體化時隻需把HashMap改成WeakHashMap,其它代碼都不用變,就這麼簡單。

l Reference queque

一旦WeakReference.get()傳回null,它指向的對象被垃圾回收,WeakReference對象就一點用都沒有了,如果要對這些沒有的WeakReference做些清理工作怎麼辦?比如在WeakHashMap中要把回收過的key從Map中删除掉。jdk中的ReferenceQueue類使你可以很容易地跟蹤dead references。WeakReference類的構造函數有一個ReferenceQueue參數,當指向的對象被垃圾回收時,會把WeakReference對象放到ReferenceQueue中。這樣,周遊ReferenceQueue可以得到所有回收過的WeakReference。WeakHashMap的做法是在每次調用size()、get()等操作時都先周遊ReferenceQueue,處理那些回收過的key,見jdk的源碼WeakHashMap# expungeStaleEntries()。

l Different degrees of weakness

上面我們僅僅提到“weak reference”,實際上根據弱的層度不同有四種引用:強(strong)、軟(soft)、弱(weak)、虛(phantom)。我們已經讨論過strong和weak,下面看下soft和phantom。

n Soft reference

Soft reference和weak reference的差別是:一旦gc發現對象是weak reference可達就會把它放到ReferenceQueue中,然後等下次gc時回收它;當對象是Soft reference可達時,gc可能會向作業系統申請更多記憶體,而不是直接回收它,當實在沒轍了才回收它。像cache系統,最适合用Soft reference。

n Phantom reference

虛引用Phantom reference與Soft reference和WeakReference的使用有很大的不同:它的get()方法總是傳回null(不信可以看jdk的PhantomReference源碼)。這意味着你隻能用PhantomReference本身,而得不到它指向的對象。它的唯一用處是你能夠在ReferenceQueue中知道它被回收了。為何要有這種“不同”?

何時進入ReferenceQueue産生了這種“不同”。WeakReference是在它指向的對象變得弱可達(weakly reachable)時立即被放到ReferenceQueue中,這在finalization、garbage collection之前發生。理論上,你可以在finalize()方法中使對象“複活”(使一個強引用指向它就行了,gc不會回收它),但WeakReference已經死了(死了?不太明白作者的确切意思。在finalize中複活對象不太能夠說明問題。理論上你可以複活ReferenceQueue中的WeakReference指向的對象,但沒法複活PhantomReference指向的對象,我想這才是它們的“不同”)。而PhantomReference不同,它是在garbage collection之後被放到ReferenceQueue中的,沒法複活。

PhantomReferences的價值在哪裡?我隻說兩點:1、你能知道一個對象已經從記憶體中删除掉了,事實上,這是唯一的途徑。這可能不是很有用,隻能用在某些特别的場景中,比如維護巨大的圖檔:隻有圖檔對象被回收之後才有必要再載入,這在很大程度上可以避免OutOfMemoryError。2、可以避免finalize()方法的缺點。在finalize方法中可以通過建立強引用來使對象複活。你可能要說,那又怎麼樣?——finalize的問題是對那些重載了finalize方法的對象垃圾回收器必須判斷兩遍才能決定回收它。第一遍,判斷對象是否可達,如果不可達,看是否有finalization,如果有則調用,否則回收;第二遍判斷對象是否可達,如果不可達,則回收。由于finalize是在記憶體回收之前調用的,那麼在finalize中可能出現OutOfMemoryError,即使很多對象可以被回收。用PhantomReference就不會出現這種情況,當PhantomReference進入ReferenceQueue之後就沒法再獲得所指向的對象(它已經從記憶體中删除了)。由于PhantomReference不能使對象複活,是以它指向的對象可以在第一遍時回收,有finalize方法的對象就不行。可以證明,finalize方法不是首選。PhantomReference更安全更有效,可以簡化VM的工作。雖然好處多,但要寫的代碼也多。是以我坦白承認,大部分情況我還是用finalize。不管怎麼樣,你多了個選擇,不用在finalize這棵樹上吊死。

l 總結

我打賭有人在嘟囔,說我在講老黃曆,沒什麼鮮貨。你說得沒錯,不過,以我的經驗仍有很多java工程師對weak reference沒甚了解,這樣一堂入門課對他們很有必要。真心希望你能從這篇文章中得到一點收獲。

繼續閱讀