天天看點

Java弱引用(WeakReferences)

  前一段時間當我面試有些來應聘進階java開發工程師崗位的候選人時,在我問的衆多問題中,有個問題是“你能告訴我弱引用是啥嗎”,我不期望得到像論文中的細節一樣的答案。我很可能從有個20多年的老工程師口中得到“嗯……是不是和gc有關”這樣的答案,所有哪些至少有5年以上經驗的工程師隻有兩個人知道弱引用的存在,隻有其中一個知道引用的相關知識。我甚至嘗試給他們解釋下看是否有人會有“哦,原來是這樣”的反應,然而并沒有。我不确定為啥這個知識點鮮為人知,但自Java1.2之後釋出的弱引用确實是有個非常有用的功能。  

  雖然作為一個java工程師我不建議你成為弱引用的專家,但我認為你至少應該知道他們是啥。換句話說你應該知道如何用他們。一直以來弱引用貌似是一個鮮為人知的功能,這裡簡單介紹下弱引用,以及如何使用和何時使用他們。

強引用(Strong references)

  首先我們需要先來複習下強引用,強引用就是你每天在java中用到的最常見的引用,例如:

StringBuffer buffer = new StringBuffer();           

  上面一行代碼建立了一個StringBuffer對象,并且用一個變量buffer存儲了它的強引用。是的,就是這麼簡單,但請耐心聽我說完。強引用最重要的部分,它強在哪裡?是如何和gc互動的? 明确的說,如果一個對象通過強引用鍊可達,它就不會被gc掉。因為誰也不希望垃圾收集器毀掉我們正在用的對象。

強應用太強?  

  應用程式使用不能合理的繼承的類的情況并不少見,這些類可能被簡單标記為final,或者更複雜一些,比如由工廠方法傳回的接口,該方法由數量未知(甚至不可知)的具體實作支援。假設你必須使用Widget類,但因為某些原因,不可能添加新功能。  

  如果你想持續追蹤這個對象的額外資訊會發生什麼? 這種情況下,假設我們需要跟蹤每個Widget的序列号,但是Widget類實際上沒有序列号屬性,而且因為Widget不能繼承,我們也加不了。沒關系,我們可以用hashmap。

serialNumberMap.put(widget, widgetSerialNumber);           

  表面上看起來可以了,但widget的強引用肯定會導緻問題。我們必須百分百确定何時Widget的序列号沒有在被用了,然後我們可以從map中移除這個實體。否則就會發生記憶體洩露(如果未移除不用的widget)或者莫名其妙的丢失序列号(如果移除還在用的widget)。這些問題聽起來很熟悉吧,這是那些沒有gc的語言在嘗試管理記憶體時遇到的問題,在java這樣的現代語言中,我們不用擔心這個問題。

  另一個常見的強引用問題就是緩存中,尤其是緩存像圖檔那樣非常大的資料時。假設你一個給使用者提供圖檔的應用,就像網頁設計應用工具。你很自然的想到去緩存那些圖檔,因為從硬碟加載成本太高了,并且你也希望避免在記憶體中存在兩份圖檔副本的可能性。

  因為圖檔緩存應該可以避免我們每次都重新加載圖檔,但你會很快意識到cache任何時候都會包含已經加載到記憶體中圖檔的引用。但是,對于普通的強引用,該引用本身将強制圖檔保留在記憶體中,這就要求你(如上所述)以某種方式确定何時不再需要該圖檔,并将其從緩存中删除,這樣它就有能被gc掉了。你又被迫重複實作了垃圾收集器的功能。

弱引用(Weak references)

  弱引用,簡單說就是不是那麼能夠強到讓對象保持在記憶體中的應用。 弱引用能讓你擁有GC的能力,讓你能确定對象的可達性。你不用自己做,你隻需要像下面一樣建立一個弱引用就行了。

WeakReference<Widget> weakWidget = new WeakReference<Widget>(widget);           

  在代碼的其他地方你就可以用weakWidget.get() 真正的Widget對象了。弱應用沒有強大到能阻擋GC,是以你會發現當沒有強引用指向widget時,weakWidget.get()會傳回null。

  為了解決上文提到的widget序列号的問題,最簡單的方式用就是用WeakHashMap,WeakHashMap和HashMap的工作方式很像,除了WeakHashMap把key替換為弱引用(不是Value),如果WeakHashMap的key變成了垃圾對象,整個entry會被自動清除。這種方式避免了我提到的陷阱,而且也隻是需要把HashMap替換為WeakHashMap就足夠了。如果你代碼遵循Map的接口标準,甚至都不需要改其他代碼。

引用隊列(Reference queues)

  一旦弱引用開始傳回null,它指向的對象肯定已經被gc掉了,弱引用對象也沒啥用了。通常這意味着可以做一些清理工作了。對于WeakHashMap而言,它會清理到沒用的entry,進而避免存着越來越多的死弱引用。

  引用隊列讓跟蹤死引用變得容易。如果你給WeakReference傳一個ReferenceQuene的構造參數,當弱引用所指向的對象變成垃圾對象後,引用對象會被自動插入到引用隊列中。然後你就可以通過引用隊列裡的對象來做一些必要的清理工作了。

各種不同強度的應用 Different degrees of weakness

  除了上面我提到的弱引用外,其實java總共有4中不同的引用,其引用強度從強到弱分别是強應用、軟引用、弱引用、虛引用。我們上文已經讨論過強應用和弱引用,接下來我們看下軟引用和虛引用。

軟引用(Soft references)

  軟引用和弱引用很想,除了它并沒有弱引用那麼急着想扔掉它引用的對象。一個隻被弱引用引用的對象會在下次gc的時候被處理掉,但被軟引用引用的對象會存在一段時間。

  軟引用和弱引用行為沒啥不同,但在實際過程中,隻要記憶體足夠,軟引用引用的對象會一直被保留。這是作為緩存很好的一個基礎,比如上面提到的圖檔緩存問題,然後你就可以讓gc去考慮哪些對象可達和這些對象消耗了多少記憶體。

虛引用(Phantom references)

  虛引用和軟引用、弱引用都不同。他對對象的應用非常弱,弱到你都不能通過get方法擷取的對象(get始終傳回null)。他隻能用來跟蹤某個對象何時進入引用隊列,隻要它進隊列了,就說明對象已死,但這和弱引用有什麼差別?

  差別就是入隊的發生發生時間不一樣。弱引用隻要對象變成弱可達就入隊列,是在finalization和GC之前,理論上,對象可以被某些非正規的finalize複活,但指向其的弱引用則不會。虛引用隻會在對象從記憶體中移除時入隊,get()始終傳回null是為了防止你複活将死的對象。

  那虛引用有什麼好的地方?我隻列舉兩點。首先,它可以讓你判斷是否一個對象已經被從記憶體中删除,事實上隻有這一種方法判斷,大部分情況下這個沒啥用,但在某些非常特殊的情況下,比如操作大型圖像時,它可能會派上用場:如果您确定某個映像應該被gc掉,那麼你可以等到它确實被gc之後再嘗試加載下一個圖檔,進而低OutOfMemoryError發生的可能性。

  其次,虛引用避免了finalize()通過建立強應用複活一個對象的問題。你說啥?問題是如果一個對象重載了finalize()方法,通過兩次gc周期它才能被回收。第一次是确定它是否是垃圾對象,然後它就變成finalization。因為有可能它在finalization過程中會被複活,gc收集器必須重新gc來確定對象被真正去除掉。并且由于finalization可能沒有及時發生,是以在對象再被gc掉前可以經曆了非常多次的gc周期。 這可能意味着實際清理垃圾對象的嚴重延遲,這就是為什麼即使堆裡大多數對象都是垃圾也會導緻OutOfMemoryErrors。

  用虛引用,這種情況是不可能出現的,絕對沒有方法擷取到一個指向已死對象的指針(因為已經不在記憶體裡了)。因為虛引用不能用來複活一個對象,這個對象可以在gc的第一階段發現隻有虛引用引用的時候被清理掉。然後你可以在友善的時候處理你需要的任何資源。

  可以說,finalize()最開始就不應當被提供。虛引用比finalize()更加高效和安全,放棄finalize()也可以讓VM更簡單。還有很長的路要走,我承認我大多數時候仍然用finalize(),但好消息是你至少有個選擇。

結論

  看到這你肯定已經在發惱騷了,因為我正在給你們講已經有近10年曆史的api,而且也沒講新内容。 但這确實是事實,好多java程式猿真的不了解弱引用,而且也需要學習下。我希望你能從這篇文章學到一些東西。

參考資料

  1. Understanding Weak References(原文)
  2. 了解Java中的弱引用