在本文中,他將解釋 Reference 對象的另外一種形式,即軟引用(soft references),用於幫助垃圾收集器治理內存使用和消除潛在的內存洩漏。
垃圾收集可以使 Java 程序不會出現內存洩漏,至少對於比較狹窄的 “內存洩漏” 定義來說如此,但是這並不意味著我們可以完全忽略 Java 程序中的對象生存期(lifetime)問題。當我們沒有對對象生命周期(lifecycle)引起足夠的重視或者破壞了治理對象生命周期的標准機制時,Java 程序中通常就會出現內存洩漏。例如,上一次 我們看到了,不能劃分對象的生命周期會導致,在試圖將元數據關聯到瞬時對象時出現意外的對象保持。還有一些其他的情況可以類似地忽略或破壞對象生命周期治理,並導致內存洩漏。
對象游離
一種形式的內存洩漏有時候叫做對象游離(object loitering),是通過清單 1 中的 LeakyChecksum 類來說明的,清單 1 中有一個 getFileChecksum() 方法用於計算文件內容的校驗和。getFileChecksum() 方法將文件內容讀取到緩沖區中以計算校驗和。一種更加直觀的實現簡單地將緩沖區作為 getFileChecksum() 中的本地變量分配,但是該版本比那樣的版本更加 “聰明”,不是將緩沖區緩存在實例字段中以減少內存 churn。該 “優化”通常不帶來預期的好處;對象分配比很多人期望的更便宜。(還要注重,將緩沖區從本地變量提升到實例變量,使得類若不帶有附加的同步,就不再是線程安全的了。直觀的實現不需要將 getFileChecksum() 聲明為 synchronized,並且會在同時調用時提供更好的可伸縮性。)
清單 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
}
}
這個類存在很多的問題,但是我們著重來看內存洩漏。緩存緩沖區的決定很可能是根據這樣的假設得出的,即該類將在一個程序中被調用許多次,因此它應該更加有效,以重用緩沖區而不是重新分配它。但是結果是,緩沖區永遠不會被釋放,因為它對程序來說總是可及的(除非 LeakyChecksum 對象被垃圾收集了)。更壞的是,它可以增長,卻不可以縮小,所以 LeakyChecksum 將永久保持一個與所處理的最大文件一樣大小的緩沖區。退一萬步說,這也會給垃圾收集器帶來壓力,並且要求更頻繁的收集;為計算未來的校驗和而保持一個大型緩沖區並不是可用內存的最有效利用。
LeakyChecksum 中問題的原因是,緩沖區對於 getFileChecksum() 操作來說邏輯上是本地的,但是它的生命周期已經被人為延長了,因為將它提升到了實例字段。因此,該類必須自己治理緩沖區的生命周期,而不是讓 JVM 來治理。
軟引用
弱引用如何可以給應用程序提供當對象被程序使用時另一種到達該對象的方法,但是不會延長對象的生命周期。Reference 的另一個子類 —— 軟引用 —— 可滿足一個不同卻相關的目的。其中弱引用答應應用程序創建不妨礙垃圾收集的引用,軟引用答應應用程序通過將一些對象指定為 “eXPendable” 而利用垃圾收集器的幫助。盡管垃圾收集器在找出哪些內存在由應用程序使用哪些沒在使用方面做得很好,但是確定可用內存的最適當使用還是取決於應用程序。假如應用程序做出了不好的決定,使得對象被保持,那麼性能會受到影響,因為垃圾收集器必須更加辛勤地工作,以防止應用程序消耗掉所有內存。
高速緩存是一種常見的性能優化,答應應用程序重用以前的計算結果,而不是重新進行計算。高速緩存是 CPU 利用和內存使用之間的一種折衷,這種折衷理想的平衡狀態取決於有多少內存可用。若高速緩存太少,則所要求的性能優勢無法達到;若太多,則性能會受到影響,因為太多的內存被用於高速緩存上,導致其他用途沒有足夠的可用內存。因為垃圾收集器比應用程序更適合決定內存需求,所以應該利用垃圾收集器在做這些決定方面的幫助,這就是件引用所要做的。
假如一個對象惟一剩下的引用是弱引用或軟引用,那麼該對象是軟可及的(softly reachable)。垃圾收集器並不像其收集弱可及的對象一樣盡量地收集軟可及的對象,相反,它只在真正 “需要” 內存時才收集軟可及的對象。軟引用對於垃圾收集器來說是這樣一種方式,即 “只要內存不太緊張,我就會保留該對象。但是假如內存變得真正緊張了,我就會去收集並處理這個對象。” 垃圾收集器在可以拋出 OutOfMemoryError 之前需要清除所有的軟引用。
通過使用一個軟引用來治理高速緩存的緩沖區,可以解決 LeakyChecksum 中的問題,如清單 2 所示。現在,只要不是非凡需要內存,緩沖區就會被保留,但是在需要時,也可被垃圾收集器回收:
清單 2. 用軟引用修復 LeakyChecksum
public class CachingChecksum {
private SoftReference
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
}
}