Java中的弱引用詳解

技術小黑屋發表於2014-12-03

不久之前,我面試了一些求職Java高階開發工程師的應聘者。我常常會面試他們說,“你能給我介紹一些Java中得弱引用嗎?”,如果面試者這樣說,“嗯,是不是垃圾回收有關的?”,我就會基本滿意了,我並不期待回答是一篇詰究本末的論文描述。

然而事與願違,我很吃驚的發現,在將近20多個有著平均5年開發經驗和高學歷背景的應聘者中,居然只有兩個人知道弱引用的存在,但是在這兩個人之中只有一個人真正瞭解這方面的知識。在面試過程中,我還嘗試提示一些東西,來看看有沒有人突然說一聲“原來是這個啊”,結果很是讓我失望。我開始困惑,為什麼這塊的知識如此不被重視,畢竟弱引用是一個很有用途的特性,況且這個特性已經在7年前 Java 1.2釋出時便引入了。

好吧,這裡我不期待你看完本文之後成為一個弱引用方面的專家,但是我認為至少你應該瞭解什麼是弱引用,如何使用它們,並且什麼場景使用。既然它們是一些不知名的概念,我簡單就著前面的三個問題來說明一下。

強引用(Strong Reference)

強引用就是我們經常使用的引用,其寫法如下

StringBuffer buffer = new StringBuffer();

上面建立了一個StringBuffer物件,並將這個物件的(強)引用存到變數buffer中。是的,就是這個小兒科的操作(請原諒我這樣的說法)。強引用最重要的就是它能夠讓引用變得強(Strong),這就決定了它和垃圾回收器的互動。具體來說,如果一個物件通過一串強引用連結可到達(Strongly reachable),它是不會被回收的。如果你不想讓你正在使用的物件被回收,這就正是你所需要的。

但是強引用如此之強

在一個程式裡,將一個類設定成不可被擴充套件是有點不太常見的,當然這個完全可以通過類標記成final實現。或者也可以更加複雜一些,就是通過內部包含了未知數量具體實現的工廠方法返回一個介面(Interface)。舉個例子,我們想要使用一個叫做Widget的類,但是這個類不能被繼承,所以無法增加新的功能。

但是我們如果想追蹤Widget物件的額外資訊,我們該怎麼辦? 假設我們需要記錄每個物件的序列號,但是由於Widget類並不包含這個屬性,而且也不能擴充套件導致我們也不能增加這個屬性。其實一點問題也沒有,HashMap完全可以解決上述的問題。

serialNumberMap.put(widget, widgetSerialNumber);

這表面看上去沒有問題,但是widget物件的強引用很有可能會引發問題。我們可以確信當一個widget序列號不需要時,我們應該將這個條目從map中移除。如果我們沒有移除的話,可能會導致記憶體洩露,亦或者我們手動移除時刪除了我們正在使用的widgets,會導致有效資料的丟失。其實這些問題很類似,這就是沒有垃圾回收機制的語言管理記憶體時常遇到的問題。但是我們不用去擔心這個問題,因為我們使用的時具有垃圾回收機制的Java語言。

另一個強引用可能帶來的問題就是快取,尤其是像圖片這樣的大檔案的快取。假設你有一個程式需要處理使用者提供的圖片,通常的做法就是做圖片資料快取,因為從磁碟載入圖片代價很大,並且同時我們也想避免在記憶體中同時存在兩份一樣的圖片資料。

快取被設計的目的就是避免我們去再次載入哪些不需要的檔案。你會很快發現在快取中會一直包含一個到已經指向記憶體中圖片資料的引用。使用強引用會強制圖片資料留在記憶體,這就需要你來決定什麼時候圖片資料不需要並且手動從快取中移除,進而可以讓垃圾回收器回收。因此你再一次被強制做垃圾回收器該做的工作,並且人為決定是該清理到哪一個物件。

弱引用(Weak Reference)

弱引用簡單來說就是將物件留在記憶體的能力不是那麼強的引用。使用WeakReference,垃圾回收器會幫你來決定引用的物件何時回收並且將物件從記憶體移除。建立弱引用如下

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

使用weakWidget.get()就可以得到真實的Widget物件,因為弱引用不能阻擋垃圾回收器對其回收,你會發現(當沒有任何強引用到widget物件時)使用get時突然返回null。

解決上述的widget序列數記錄的問題,最簡單的辦法就是使用Java內建的WeakHashMap類。WeakHashMap和HashMap幾乎一樣,唯一的區別就是它的鍵(不是值!!!)使用WeakReference引用。當WeakHashMap的鍵標記為垃圾的時候,這個鍵對應的條目就會自動被移除。這就避免了上面不需要的Widget物件手動刪除的問題。使用WeakHashMap可以很便捷地轉為HashMap或者Map。

引用佇列(Reference Queue)

一旦弱引用物件開始返回null,該弱引用指向的物件就被標記成了垃圾。而這個弱引用物件(非其指向的物件)就沒有什麼用了。通常這時候需要進行一些清理工作。比如WeakHashMap會在這時候移除沒用的條目來避免儲存無限制增長的沒有意義的弱引用。

引用佇列可以很容易地實現跟蹤不需要的引用。當你在構造WeakReference時傳入一個ReferenceQueue物件,當該引用指向的物件被標記為垃圾的時候,這個引用物件會自動地加入到引用佇列裡面。接下來,你就可以在固定的週期,處理傳入的引用佇列,比如做一些清理工作來處理這些沒有用的引用物件。

四種引用

Java中實際上有四種強度不同的引用,從強到弱它們分別是,強引用,軟引用,弱引用和虛引用。上面部分介紹了強引用和弱引用,下面介紹剩下的兩個,軟引用和虛引用。

軟引用(Soft Reference)

軟引用基本上和弱引用差不多,只是相比弱引用,它阻止垃圾回收期回收其指向的物件的能力強一些。如果一個物件是弱引用可到達,那麼這個物件會被垃圾回收器接下來的回收週期銷燬。但是如果是軟引用可以到達,那麼這個物件會停留在記憶體更時間上長一些。當記憶體不足時垃圾回收器才會回收這些軟引用可到達的物件。

由於軟引用可到達的物件比弱引用可達到的物件滯留記憶體時間會長一些,我們可以利用這個特性來做快取。這樣的話,你就可以節省了很多事情,垃圾回收器會關心當前哪種可到達型別以及記憶體的消耗程度來進行處理。

虛引用 (Phantom Reference)

與軟引用,弱引用不同,虛引用指向的物件十分脆弱,我們不可以通過get方法來得到其指向的物件。它的唯一作用就是當其指向的物件被回收之後,自己被加入到引用佇列,用作記錄該引用指向的物件已被銷燬。

當弱引用的指向物件變得弱引用可到達,該弱引用就會加入到引用佇列。這一操作發生在物件析構或者垃圾回收真正發生之前。理論上,這個即將被回收的物件是可以在一個不符合規範的析構方法裡面重新復活。但是這個弱引用會銷燬。虛引用只有在其指向的物件從記憶體中移除掉之後才會加入到引用佇列中。其get方法一直返回null就是為了阻止其指向的幾乎被銷燬的物件重新復活。

虛引用使用場景主要由兩個。它允許你知道具體何時其引用的物件從記憶體中移除。而實際上這是Java中唯一的方式。這一點尤其表現在處理類似圖片的大檔案的情況。當你確定一個圖片資料物件應該被回收,你可以利用虛引用來判斷這個物件回收之後在繼續載入下一張圖片。這樣可以儘可能地避免可怕的記憶體溢位錯誤。

第二點,虛引用可以避免很多析構時的問題。finalize方法可以通過建立強引用指向快被銷燬的物件來讓這些物件重新復活。然而,一個重寫了finalize方法的物件如果想要被回收掉,需要經歷兩個單獨的垃圾收集週期。在第一個週期中,某個物件被標記為可回收,進而才能進行析構。但是因為在析構過程中仍有微弱的可能這個物件會重新復活。這種情況下,在這個物件真實銷燬之前,垃圾回收器需要再次執行。因為析構可能並不是很及時,所以在呼叫物件的析構之前,需要經歷數量不確定的垃圾收集週期。這就意味著在真正清理掉這個物件的時候可能發生很大的延遲。這就是為什麼當大部分堆被標記成垃圾時還是會出現煩人的記憶體溢位錯誤。

使用虛引用,上述情況將引刃而解,當一個虛引用加入到引用佇列時,你絕對沒有辦法得到一個銷燬了的物件。因為這時候,物件已經從記憶體中銷燬了。因為虛引用不能被用作讓其指向的物件重生,所以其物件會在垃圾回收的第一個週期就將被清理掉。

顯而易見,finalize方法不建議被重寫。因為虛引用明顯地安全高效,去掉finalize方法可以虛擬機器變得明顯簡單。當然你也可以去重寫這個方法來實現更多。這完全看個人選擇。

總結

我想看到這裡,很多人開始發牢騷了,為什麼你要講一個過去十年的老古董API呢,好吧,以我的經驗看,很多的Java程式設計師並不是很瞭解這個知識,我認為有一些深入的理解是很必要的,同時我希望大家能從本文中收穫一些東西。

相關文章