級 別: 中級 Brian Goetz (brian@quiotix.com), Principal Consultant, Quiotix
2006 年 3 月 20 日 在 Java 理論和實踐 的 前 一期文章 中,Java™ 清潔工程師 Brian Goetz 探究了弱引用(weak references),它讓您警告垃 圾收集器,您想要維護一個對象的引用,而不會阻止該對象被垃圾收集。在本期文章中,他將解釋 Reference 對象的另外一種形式,即軟引用(soft references),用于幫助垃圾收集器管理內(nèi)存使用和消除潛在的內(nèi)存泄漏。 垃圾收集可以使 Java 程序不會出現(xiàn)內(nèi)存泄漏,至少對于比較狹窄的 “內(nèi)存泄漏” 定義來說如此,但是這并不意味著我們可以完全忽略 Java 程序中的對象生存期(lifetime)問題。當(dāng)我們沒有對對象生命周期(lifecycle)引起足夠的重視或者破壞了管理對象生命周期的標準機制 時,Java 程序中通常就會出現(xiàn)內(nèi)存泄漏。例如,上 一次 我們看到了,不能劃分對象的生命周期會導(dǎo)致,在試圖將元數(shù)據(jù)關(guān)聯(lián)到瞬時對象時出現(xiàn)意外的對象保持。還有一些其他的情況可以類似地忽略或破壞對象生命周期管 理,并導(dǎo)致內(nèi)存泄漏。 對象游離 一種形式的內(nèi)存泄漏有時候叫做對象游離(object loitering),是通過清單 1 中的 LeakyChecksum 類來說明的,清單 1 中有一個 getFileChecksum() 方法用于計算文件內(nèi)容的校驗和。getFileChecksum() 方法將文件內(nèi)容讀取到緩沖區(qū)中以計算校驗和。一種更加直觀的實現(xiàn)簡單地將緩沖區(qū)作為 getFileChecksum() 中的本地變量分配,但是該版本比那樣的版本更加 “聰明”,不是將緩沖區(qū)緩存在實例字段中以減少內(nèi)存 churn。該 “優(yōu)化”通常不帶來預(yù)期的好處;對象分配比很多人期望的更便宜。(還要注意,將緩沖區(qū)從本地變量提升到實例變量,使得類若不帶有附加的同步,就不再是線程 安全的了。直觀的實現(xiàn)不需要將 getFileChecksum() 聲明為 synchronized , 并且會在同時調(diào)用時提供更好的可伸縮性。) 清單 1. 展示 “對象游離” 的類 // BAD CODE - DO NOT EMULATE public class LeakyChecksum { private byte[] byteArray; public synchronized int getFileChecksum(String fileName) { int len = getFileSize(fileName); if (byteArray == null || byteArray.length < len) byteArray = new byte[len]; readFileContents(fileName, byteArray); // calculate checksum and return it } }
| 這個類存在很多的問題,但是我們著重來看內(nèi)存泄漏。緩存緩沖區(qū)的決定很可能是根據(jù)這樣的假設(shè)得出的,即該類將在一個程序中被調(diào)用許多次,因此它應(yīng)該更加有 效,以重用緩沖區(qū)而不是重新分配它。但是結(jié)果是,緩沖區(qū)永遠不會被釋放,因為它對程序來說總是可及的(除非 LeakyChecksum 對象被垃圾收集了)。更壞的是,它可以增長,卻不可以縮小,所以 LeakyChecksum 將永久保持一個與所處理的最大文件一樣大小的緩沖區(qū)。退一萬步說,這也會給垃圾收集器帶來壓力,并且要求更頻繁的收集;為計算未來的校驗和而保持一個大型 緩沖區(qū)并不是可用內(nèi)存的最有效利用。 LeakyChecksum 中問題的原因是,緩沖區(qū)對于 getFileChecksum() 操作來說邏輯上是本地的,但是它的生命周期已經(jīng)被人為延長了,因為將它提升到了實例字段。因此,該類必須自己管理緩沖區(qū)的生命周期,而不是讓 JVM 來管理。
軟引用 在 前 一期文章 中我們看到了,弱引用如何可以給應(yīng)用程序提供當(dāng)對象被程序使用時另一種到達該對象的方法,但是不會延長對象的生命周期。Reference 的另一個子類 —— 軟引用 —— 可滿足一個不同卻相關(guān)的目的。其中弱引用允許應(yīng)用程序創(chuàng)建不妨礙垃圾收集的引用,軟引用允許應(yīng)用程序通過將一些對象指定為 “expendable” 而利用垃圾收集器的幫助。盡管垃圾收集器在找出哪些內(nèi)存在由應(yīng)用程序使用哪些沒在使用方面做得很好,但是確定可用內(nèi)存的最適當(dāng)使用還是取決于應(yīng)用程序。如 果應(yīng)用程序做出了不好的決定,使得對象被保持,那么性能會受到影響,因為垃圾收集器必須更加辛勤地工作,以防止應(yīng)用程序消耗掉所有內(nèi)存。 高速緩存是一種常見的性能優(yōu)化,允許應(yīng)用程序重用以前的計算結(jié)果,而不是重新進行計算。高速緩存是 CPU 利用和內(nèi)存使用之間的一種折衷,這種折衷理想的平衡狀態(tài)取決于有多少內(nèi)存可用。若高速緩存太少,則所要求的性能優(yōu)勢無法達到;若太多,則性能會受到影響, 因為太多的內(nèi)存被用于高速緩存上,導(dǎo)致其他用途沒有足夠的可用內(nèi)存。因為垃圾收集器比應(yīng)用程序更適合決定內(nèi)存需求,所以應(yīng)該利用垃圾收集器在做這些決定方 面的幫助,這就是件引用所要做的。 如果一個對象惟一剩下的引用是弱引用或軟引用,那么該對象是軟可及的(softly reachable)。垃圾收集器并不像其收集弱可及的對象一樣盡量地收集軟可及的對象,相反,它只在真正 “需要” 內(nèi)存時才收集軟可及的對象。軟引用對于垃圾收集器來說是這樣一種方式,即 “只要內(nèi)存不太緊張,我就會保留該對象。但是如果內(nèi)存變得真正緊張了,我就會去收集并處理這個對象。” 垃圾收集器在可以拋出 OutOfMemoryError 之前需要清除所有的軟引用。 通過使用一個軟引用來管理高速緩存的緩沖區(qū),可以解決 LeakyChecksum 中的問題,如清單 2 所示?,F(xiàn)在,只要不是特別需要內(nèi)存,緩沖區(qū)就會被保留,但是在需要時,也可被垃圾收集器回收: 清單 2. 用軟引用修復(fù) LeakyChecksum public class CachingChecksum { private SoftReference<byte[]> bufferRef; public synchronized int getFileChecksum(String fileName) { int len = getFileSize(fileName); byte[] byteArray = bufferRef.get(); if (byteArray == null || byteArray.length < len) { byteArray = new byte[len]; bufferRef.set(byteArray); } readFileContents(fileName, byteArray); // calculate checksum and return it } }
| 一種廉價的緩存 CachingChecksum 使用一個軟引用來緩存單個對象,并讓 JVM 處理從緩存中取走對象時的細節(jié)。類似地,軟引用也經(jīng)常用于 GUI 應(yīng)用程序中,用于緩存位圖圖形。是否可使用軟引用的關(guān)鍵在于,應(yīng)用程序是否可從大量緩存的數(shù)據(jù)恢復(fù)。 如果需要緩存不止一個對象,您可以使用一個 Map ,但是可以選擇如何使用軟引用。您可以將緩存作為 Map<K, SoftReference<V>> 或 SoftReference<Map<K,V>> 管理。后一種選項通常更好一些,因為它給垃圾收集器帶來的工作更少,并且允許在特別需要內(nèi)存時以較少的工作回收整個緩存。弱引用有時會錯誤地用于取代軟引 用,用于構(gòu)建緩存,但是這會導(dǎo)致差的緩存性能。在實踐中,弱引用將在對象變得弱可及之后被很快地清除掉 —— 通常是在緩存的對象再次用到之前 —— 因為小的垃圾收集運行得很頻繁。 對于在性能上非常依賴高速緩存的應(yīng)用程序來說,軟引用是一個不管用的手段,它確實不能取代能夠提供靈活終止期、復(fù)制和事務(wù)型高速緩存的復(fù)雜的高速緩存框 架。但是作為一種 “廉價(cheap and dirty)” 的高速緩存機制,它對于降低價格是很有吸引力的。 正如弱引用一樣,軟引用也可創(chuàng)建為具有一個相關(guān)的引用隊列,引用在被垃圾收集器清除時進入隊列。引用隊列對于軟引用來說,沒有對弱引用那么有用,但是它們 可以用于發(fā)出管理警報,說明應(yīng)用程序開始缺少內(nèi)存。
垃圾收集器如何處理 References 弱引用和軟引用都擴展了抽象的 Reference 類(虛引用(phantom references)也一 樣,這將在以后的文章中介紹)。引用對象被垃圾收集器特殊地看待。垃圾收集器在跟蹤堆期間遇到一個 Reference 時,不會標記或跟蹤該引用對象,而是在已知活躍的 Reference 對象的隊列上放置一個 Reference 。 在跟蹤之后,垃圾收集器就識別軟可及的對象 —— 這些對象上除了軟引用外,沒有任何強引用。垃圾收集器然后根據(jù)當(dāng)前收集所回收的內(nèi)存總量和其他策略考慮因素,判斷軟引用此時是否需要被清除。將被清除的軟 引用如果具有相應(yīng)的引用隊列,就會進入隊列。其余的軟可及對象(沒有清除的對象)然后被看作一個根集(root set),堆跟蹤繼續(xù)使用這些新的根,以便通過活躍的軟引用而可及的對象能夠被標記。 處理軟引用之后,弱可及對象的集合被識別 —— 這樣的對象上不存在強引用或軟引用。這些對象被清除和加入隊列。所有 Reference 類型在加入隊列之前被清除,所以處理事后檢查(post-mortem)清除的線程永遠不會具有 referent 對象的訪問權(quán),而只具有 Reference 對象的訪問權(quán)。因此,當(dāng) References 與引用隊列一起使用時,通常需要細分適當(dāng)?shù)囊妙愋?,并將它直接用于您的設(shè)計中(與 WeakHashMap 一樣,它的 Map.Entry 擴展了 WeakReference )或者存儲對需要清除的實體的引用。 引用處理的性能成本 引用對象給垃圾收集過程帶來了一些附加的成本。每一次垃圾收集,都必須構(gòu)造活躍 Reference 對象的一個列表,而且每個引用都必須做適當(dāng)?shù)奶幚?,這給每次收集添加了一些每個 Reference 的開銷,而不管該 referent 此時是否被收集。Reference 對象本身服從于垃圾收集,并且可在 referent 之前被收集,在這樣的情況下,它們沒有加入隊列。
基于數(shù)組的集合 當(dāng)數(shù)組用于實現(xiàn)諸如堆?;颦h(huán)形緩沖區(qū)之類的數(shù)據(jù)結(jié)構(gòu)時,會出現(xiàn)另一種形式的對象游離。清單 3 中的 LeakyStack 類展示了用數(shù)組實現(xiàn)的堆棧的實現(xiàn)。在 pop() 方法中,在頂部指針遞減之后,elements 仍然會保留對將彈出堆棧的對象的引用。這意味著,該對象的引用對程序來說仍然可及(即使程序?qū)嶋H上不會再使用該引用),這會阻止該對象被垃圾收集,直到該 位置被未來的 push() 重用。 清單 3. 基于數(shù)組的集合中的對象游離 public class LeakyStack { private Object[] elements = new Object[MAX_ELEMENTS]; private int size = 0; public void push(Object o) { elements[size++] = o; } public Object pop() { if (size == 0) throw new EmptyStackException(); else { Object result = elements[--size]; // elements[size+1] = null; return result; } } }
| 修復(fù)這種情況下的對象游離的方法是,當(dāng)對象從堆棧彈出之后,就消除它的引用,如清單 3 中注釋掉的行所示。但是這種情況 —— 由類管理其自己的內(nèi)存 —— 是一種非常少見的情況,即顯式地消除不再需要的對象是一個好主意。大部分時候,認為不應(yīng)該使用的強行消除引用根本不會帶來性能或內(nèi)存使用方面的收益,通常 是導(dǎo)致更差的性能或者 NullPointerException 。該算法的一個鏈接實現(xiàn)不會存在這個問題。在鏈接實現(xiàn)中,鏈接節(jié)點(以及所存儲的對 象的引用)的生命期將被自動與對象存儲在集合中的期間綁定在一起。弱引用可用于解決這個問題 —— 維護弱引用而不是強引用的一個數(shù)組 —— 但是在實際中,LeakyStack 管理它自己的內(nèi)存,因此負責(zé)確保對不再需要的對象的引用被清除。使用數(shù)組來實現(xiàn)堆?;蚓彌_區(qū)是一種優(yōu)化,可以減少分配,但是會給實現(xiàn)者帶來更大的負擔(dān),需 要仔細地管理存儲在數(shù)組中的引用的生命期。
結(jié)束語 與弱引用一樣,軟引用通過利用垃圾收集器在作出緩存回收決策方面的幫助,有助于防止應(yīng)用程序出現(xiàn)對象游離。只有當(dāng)應(yīng)用程序可以忍受大量軟引用的對象時,軟 引用才適合使用。
參考資料 學(xué) 習(xí) 獲得產(chǎn)品和技術(shù) - JTune:這個 免費的 JTune 工具可以利用 GC 日志并以圖表形式顯示堆大小、GC 期間和其他有用的內(nèi)存管理數(shù)據(jù)。
討 論
關(guān)于作者 |
| | Brian Goetz has been a professional software developer for over 18 years. He is a Principal Consultant at Quiotix, a software development and consulting firm located in Los Altos, California, and he serves on several JCP Expert Groups. Brian's book, Java Concurrency In Practice , will be published in late 2005 by Addison-Wesley. See Brian's published and upcoming articles in popular industry publications. | |