精品主題,實(shí)戰(zhàn)科普,最新行業(yè)熱點(diǎn)話題,隨時(shí)掌握云上咨訊。
不久之前,我面試了一些求職 Java 高級(jí)開發(fā)工程師的應(yīng)聘者。我常常會(huì)面試他們說,“你能給我介紹一些 Java 中得弱引用嗎?”,如果面試者這樣說,“嗯,是不是垃圾回收有關(guān)的?”,我就會(huì)基本滿意了,我并不期待回答是一篇詰究本末的論文描述。
然而事與愿違,我很吃驚的發(fā)現(xiàn),在將近 20 多個(gè)有著平均 5 年開發(fā)經(jīng)驗(yàn)和高學(xué)歷背景的應(yīng)聘者中,居然只有兩個(gè)人知道弱引用的存在,但是在這兩個(gè)人之中只有一個(gè)人真正了解這方面的知識(shí)。在面試過程中,我還嘗試提示一些東西,來看看有沒有人突然說一聲“原來是這個(gè)啊”,結(jié)果很是讓我失望。我開始困惑,為什么這塊的知識(shí)如此不被重視,畢竟弱引用是一個(gè)很有用途的特性,況且這個(gè)特性已經(jīng)在 7 年前 Java 1.2 發(fā)布時(shí)便引入了。
好吧,這里我不期待你看完本文之后成為一個(gè)弱引用方面的專家,但是我認(rèn)為至少你應(yīng)該了解什么是弱引用,如何使用它們,并且什么場(chǎng)景使用。既然它們是一些不知名的概念,我簡單就著前面的三個(gè)問題來說明一下。
強(qiáng)引用(Strong Reference)
強(qiáng)引用就是我們經(jīng)常使用的引用,其寫法如下
StringBuffer buffer = new StringBuffer ();
上面創(chuàng)建了一個(gè) StringBuffer 對(duì)象,并將這個(gè)對(duì)象的(強(qiáng))引用存到變量 buffer 中。是的,就是這個(gè)小兒科的操作(請(qǐng)?jiān)徫疫@樣的說法)。強(qiáng)引用最重要的就是它能夠讓引用變得強(qiáng)(Strong),這就決定了它和垃圾回收器的交互。具體來說,如果一個(gè)對(duì)象通過一串強(qiáng)引用鏈接可到達(dá)(Strongly reachable),它是不會(huì)被回收的。如果你不想讓你正在使用的對(duì)象被回收,這就正是你所需要的。
但是強(qiáng)引用如此之強(qiáng)
在一個(gè)程序里,將一個(gè)類設(shè)置成不可被擴(kuò)展是有點(diǎn)不太常見的,當(dāng)然這個(gè)完全可以通過類標(biāo)記成 final 實(shí)現(xiàn)。或者也可以更加復(fù)雜一些,就是通過內(nèi)部包含了未知數(shù)量具體實(shí)現(xiàn)的工廠方法返回一個(gè)接口(Interface)。舉個(gè)例子,我們想要使用一個(gè)叫做 Widget 的類,但是這個(gè)類不能被繼承,所以無法增加新的功能。
但是我們?nèi)绻胱粉?nbsp;Widget 對(duì)象的額外信息,我們?cè)撛趺崔k? 假設(shè)我們需要記錄每個(gè)對(duì)象的序列號(hào),但是由于 Widget 類并不包含這個(gè)屬性,而且也不能擴(kuò)展導(dǎo)致我們也不能增加這個(gè)屬性。其實(shí)一點(diǎn)問題也沒有,HashMap 完全可以解決上述的問題。
serialNumberMap.put (widget, widgetSerialNumber);
這表面看上去沒有問題,但是 widget 對(duì)象的強(qiáng)引用很有可能會(huì)引發(fā)問題。我們可以確信當(dāng)一個(gè) widget 序列號(hào)不需要時(shí),我們應(yīng)該將這個(gè)條目從 map 中移除。如果我們沒有移除的話,可能會(huì)導(dǎo)致內(nèi)存泄露,亦或者我們手動(dòng)移除時(shí)刪除了我們正在使用的 widgets,會(huì)導(dǎo)致有效數(shù)據(jù)的丟失。其實(shí)這些問題很類似,這就是沒有垃圾回收機(jī)制的語言管理內(nèi)存時(shí)常遇到的問題。但是我們不用去擔(dān)心這個(gè)問題,因?yàn)槲覀兪褂玫臅r(shí)具有垃圾回收機(jī)制的 Java 語言。
另一個(gè)強(qiáng)引用可能帶來的問題就是緩存,尤其是像圖片這樣的大文件的緩存。假設(shè)你有一個(gè)程序需要處理用戶提供的圖片,通常的做法就是做圖片數(shù)據(jù)緩存,因?yàn)閺拇疟P加載圖片代價(jià)很大,并且同時(shí)我們也想避免在內(nèi)存中同時(shí)存在兩份一樣的圖片數(shù)據(jù)。
緩存被設(shè)計(jì)的目的就是避免我們?nèi)ピ俅渭虞d哪些不需要的文件。你會(huì)很快發(fā)現(xiàn)在緩存中會(huì)一直包含一個(gè)到已經(jīng)指向內(nèi)存中圖片數(shù)據(jù)的引用。使用強(qiáng)引用會(huì)強(qiáng)制圖片數(shù)據(jù)留在內(nèi)存,這就需要你來決定什么時(shí)候圖片數(shù)據(jù)不需要并且手動(dòng)從緩存中移除,進(jìn)而可以讓垃圾回收器回收。因此你再一次被強(qiáng)制做垃圾回收器該做的工作,并且人為決定是該清理到哪一個(gè)對(duì)象。
弱引用(Weak Reference)
弱引用簡單來說就是將對(duì)象留在內(nèi)存的能力不是那么強(qiáng)的引用。使用 WeakReference,垃圾回收器會(huì)幫你來決定引用的對(duì)象何時(shí)回收并且將對(duì)象從內(nèi)存移除。創(chuàng)建弱引用如下
WeakReference
使用 weakWidget.get ()就可以得到真實(shí)的 Widget 對(duì)象,因?yàn)槿跻貌荒茏钃趵厥掌鲗?duì)其回收,你會(huì)發(fā)現(xiàn)(當(dāng)沒有任何強(qiáng)引用到 widget 對(duì)象時(shí))使用 get 時(shí)突然返回 null。
解決上述的 widget 序列數(shù)記錄的問題,最簡單的辦法就是使用 Java 內(nèi)置的 WeakHashMap 類。WeakHashMap 和 HashMap 幾乎一樣,唯一的區(qū)別就是它的鍵(不是值!!!)使用 WeakReference 引用。當(dāng) WeakHashMap 的鍵標(biāo)記為垃圾的時(shí)候,這個(gè)鍵對(duì)應(yīng)的條目就會(huì)自動(dòng)被移除。這就避免了上面不需要的 Widget 對(duì)象手動(dòng)刪除的問題。使用 WeakHashMap 可以很便捷地轉(zhuǎn)為 HashMap 或者 Map。
引用隊(duì)列(Reference Queue)
一旦弱引用對(duì)象開始返回 null,該弱引用指向的對(duì)象就被標(biāo)記成了垃圾。而這個(gè)弱引用對(duì)象(非其指向的對(duì)象)就沒有什么用了。通常這時(shí)候需要進(jìn)行一些清理工作。比如 WeakHashMap 會(huì)在這時(shí)候移除沒用的條目來避免保存無限制增長的沒有意義的弱引用。
引用隊(duì)列可以很容易地實(shí)現(xiàn)跟蹤不需要的引用。當(dāng)你在構(gòu)造 WeakReference 時(shí)傳入一個(gè) ReferenceQueue 對(duì)象,當(dāng)該引用指向的對(duì)象被標(biāo)記為垃圾的時(shí)候,這個(gè)引用對(duì)象會(huì)自動(dòng)地加入到引用隊(duì)列里面。接下來,你就可以在固定的周期,處理傳入的引用隊(duì)列,比如做一些清理工作來處理這些沒有用的引用對(duì)象。
四種引用
Java 中實(shí)際上有四種強(qiáng)度不同的引用,從強(qiáng)到弱它們分別是,強(qiáng)引用,軟引用,弱引用和虛引用。上面部分介紹了強(qiáng)引用和弱引用,下面介紹剩下的兩個(gè),軟引用和虛引用。
軟引用(Soft Reference)
軟引用基本上和弱引用差不多,只是相比弱引用,它阻止垃圾回收期回收其指向的對(duì)象的能力強(qiáng)一些。如果一個(gè)對(duì)象是弱引用可到達(dá),那么這個(gè)對(duì)象會(huì)被垃圾回收器接下來的回收周期銷毀。但是如果是軟引用可以到達(dá),那么這個(gè)對(duì)象會(huì)停留在內(nèi)存更時(shí)間上長一些。當(dāng)內(nèi)存不足時(shí)垃圾回收器才會(huì)回收這些軟引用可到達(dá)的對(duì)象。
由于軟引用可到達(dá)的對(duì)象比弱引用可達(dá)到的對(duì)象滯留內(nèi)存時(shí)間會(huì)長一些,我們可以利用這個(gè)特性來做緩存。這樣的話,你就可以節(jié)省了很多事情,垃圾回收器會(huì)關(guān)心當(dāng)前哪種可到達(dá)類型以及內(nèi)存的消耗程度來進(jìn)行處理。
虛引用 (Phantom Reference)
與軟引用,弱引用不同,虛引用指向的對(duì)象十分脆弱,我們不可以通過 get 方法來得到其指向的對(duì)象。它的唯一作用就是當(dāng)其指向的對(duì)象被回收之后,自己被加入到引用隊(duì)列,用作記錄該引用指向的對(duì)象已被銷毀。
當(dāng)弱引用的指向?qū)ο笞兊萌跻每傻竭_(dá),該弱引用就會(huì)加入到引用隊(duì)列。這一操作發(fā)生在對(duì)象析構(gòu)或者垃圾回收真正發(fā)生之前。理論上,這個(gè)即將被回收的對(duì)象是可以在一個(gè)不符合規(guī)范的析構(gòu)方法里面重新復(fù)活。但是這個(gè)弱引用會(huì)銷毀。虛引用只有在其指向的對(duì)象從內(nèi)存中移除掉之后才會(huì)加入到引用隊(duì)列中。其 get 方法一直返回 null 就是為了阻止其指向的幾乎被銷毀的對(duì)象重新復(fù)活。
虛引用使用場(chǎng)景主要由兩個(gè)。它允許你知道具體何時(shí)其引用的對(duì)象從內(nèi)存中移除。而實(shí)際上這是 Java 中唯一的方式。這一點(diǎn)尤其表現(xiàn)在處理類似圖片的大文件的情況。當(dāng)你確定一個(gè)圖片數(shù)據(jù)對(duì)象應(yīng)該被回收,你可以利用虛引用來判斷這個(gè)對(duì)象回收之后在繼續(xù)加載下一張圖片。這樣可以盡可能地避免可怕的內(nèi)存溢出錯(cuò)誤。
第二點(diǎn),虛引用可以避免很多析構(gòu)時(shí)的問題。finalize 方法可以通過創(chuàng)建強(qiáng)引用指向快被銷毀的對(duì)象來讓這些對(duì)象重新復(fù)活。然而,一個(gè)重寫了 finalize 方法的對(duì)象如果想要被回收掉,需要經(jīng)歷兩個(gè)單獨(dú)的垃圾收集周期。在第一個(gè)周期中,某個(gè)對(duì)象被標(biāo)記為可回收,進(jìn)而才能進(jìn)行析構(gòu)。但是因?yàn)樵谖鰳?gòu)過程中仍有微弱的可能這個(gè)對(duì)象會(huì)重新復(fù)活。這種情況下,在這個(gè)對(duì)象真實(shí)銷毀之前,垃圾回收器需要再次運(yùn)行。因?yàn)槲鰳?gòu)可能并不是很及時(shí),所以在調(diào)用對(duì)象的析構(gòu)之前,需要經(jīng)歷數(shù)量不確定的垃圾收集周期。這就意味著在真正清理掉這個(gè)對(duì)象的時(shí)候可能發(fā)生很大的延遲。這就是為什么當(dāng)大部分堆被標(biāo)記成垃圾時(shí)還是會(huì)出現(xiàn)煩人的內(nèi)存溢出錯(cuò)誤。
使用虛引用,上述情況將引刃而解,當(dāng)一個(gè)虛引用加入到引用隊(duì)列時(shí),你絕對(duì)沒有辦法得到一個(gè)銷毀了的對(duì)象。因?yàn)檫@時(shí)候,對(duì)象已經(jīng)從內(nèi)存中銷毀了。因?yàn)樘撘貌荒鼙挥米髯屍渲赶虻膶?duì)象重生,所以其對(duì)象會(huì)在垃圾回收的第一個(gè)周期就將被清理掉。
顯而易見,finalize 方法不建議被重寫。因?yàn)樘撘妹黠@地安全高效,去掉 finalize 方法可以虛擬機(jī)變得明顯簡單。當(dāng)然你也可以去重寫這個(gè)方法來實(shí)現(xiàn)更多。這完全看個(gè)人選擇。
總結(jié)
我想看到這里,很多人開始發(fā)牢騷了,為什么你要講一個(gè)過去十年的老古董 API 呢,好吧,以我的經(jīng)驗(yàn)看,很多的 Java 程序員并不是很了解這個(gè)知識(shí),我認(rèn)為有一些深入的理解是很必要的,同時(shí)我希望大家能從本文中收獲一些東西。