Understanding Weak References

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類自己也沒有這樣的序列號,你準備怎麼辦?用HashMapserialNumberMap.put(widget, widgetSerialNumber),用變量記錄新實例的序列號,創建實例時把實例和它的序列號放到HashMap中。很顯然,這個Map會不斷變大,從而造成內存泄漏。你要說,不要緊,在不用某個實例時就從map中刪除它。是的,這可行,但是“put——remove”,你不覺得你在做與內存管理“new——delete”類似的事嗎?像所有自己管理內存的語言一樣,你不能有遺漏。這不是java風格。

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

l         Weak reference
    Weak reference,簡單地說就是這個引用不會強到迫使對象必須保持在內存中。Gc不會碰Strong reference可達的對象,但可以碰weak reference可達的對象。下面創建一個weak referenceWeakReference 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中要把回收過的keyMap中刪除掉。jdk中的ReferenceQueue類使你可以很容易地跟蹤dead referencesWeakReference類的構造函數有一個ReferenceQueue參數,當指向的對象被垃圾回收時,會把WeakReference對象放到ReferenceQueue中。這樣,遍歷ReferenceQueue可以得到所有回收過的WeakReferenceWeakHashMap的做法是在每次調用size()get()等操作時都先遍歷ReferenceQueue,處理那些回收過的key,見jdk的源碼WeakHashMap# expungeStaleEntries()

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

n         Soft reference
      Soft referenceweak reference的區別是:一旦gc發現對象是weak reference可達就會把它放到ReferenceQueue中,然後等下次gc時回收它;當對象是Soft reference可達時,gc可能會向操作系統申請更多內存,而不是直接回收它,當實在沒轍了纔回收它。像cache系統,最適合用Soft reference

n         Phantom reference
      虛引用Phantom referenceSoft referenceWeakReference的使用有很大的不同:它的get()方法總是返回null(不信可以看jdkPhantomReference源碼)。這意味着你只能用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沒甚瞭解,這樣一堂入門課對他們很有必要。真心希望你能從這篇文章中得到一點收穫。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章