當你從手工管理內存的語言(好比C或者C++)轉換到具備垃圾回收功能的語言的時候,程序猿的工做就會變得更加容易,由於當你用完了對象以後,他們就會被自動回收。當你第一次經歷對象回收功能的時候,會以爲這簡直有點難以想象。這很容易給你留下這樣的印象,認爲本身再也不須要考慮內存管理的事情了,其實否則。java
考慮下面這個簡單的棧實現的例子:程序員
// Can you spot the "memory leak"? public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) throw new EmptyStackException(); return elements[--size]; } /** * Ensure space for at least one more element, roughly * doubling the capacity each time the array needs to grow. */ private void ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } }
這個程序沒有明顯的錯誤(它的通用版本請見29項)。不管如何測試,它都會成功地經過每一項測試,可是這個程序中隱藏着一個問題。簡而言之,改程序存在「內存泄漏」,因爲垃圾收集器的活動增長或者內存佔用增長,程序性能的下降會逐漸表現出來。在極端的狀況下,這種內存泄漏會致使磁盤分頁(Disk Paging),甚至致使程序失敗並出現OutOfMemoryError,但這種失敗情形相對比較少見。編程
那麼,程序中哪裏發生了內存泄漏呢?若是一個棧先是增加,而後再收縮,那麼,從棧中彈出來的對象將不會被當作垃圾回收,即便使用棧的程序再也不引用這些對象,它們也不會回收,由於,棧內部維護着對這些對象的過時引用(obsolete references),所謂的過時引用,是指永遠也不會再被解除的引用。在本例中,凡是在element數組的「活動部分」(active portion)以外的任何引用都是過時的。活動部分是指element中下標小於size的那些元素。數組
具備垃圾收集功能的編程語言中的內存泄漏(更恰當地稱爲無心識的對象保留)是隱蔽的。若是無心中保留了對象引用,則不只將該對象從垃圾回收中排除,並且該對象引用的任何對象也是如此,依此類推。即便無心中保留了少許對象引用,也會阻止許多對象被垃圾回收器收集,對性能可能產生很大影響。緩存
這類問題的修復方法很簡單:一旦對象引用已通過期,只須要清空這些引用便可。對於上述例子中的Stack類而言,只要一個單元被彈出棧,指向它的引用就過時了,pop方法的修訂版本以下所示:編程語言
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; }
清空過時引用的另外一個好處是,若是它們之後又被錯誤地解除引用,程序就會當即拋出NullPointerException異常,而不是悄悄地錯誤運行下去。儘快檢測出程序中的錯誤老是有益的。工具
當程序員第一次被相似這樣的問題困擾的時候,它們每每會過度當心:對於每個對象的引用,一旦程序再也不用到它,就把它清空。其實這樣作即不必,也不是咱們所指望的,由於這樣作會把程序代碼弄得很亂。清空對象引用應該是一種例外,而不是一種規範行爲。消除過時引用最好的方法是讓包含該引用的變量結束其生命週期。若是你是在最緊湊的做用域範圍內定義每個變量(第57項),這種情形就會天然而然地發生。性能
那麼,什麼時候應該清空引用呢?Stack類的哪方面特性使它易於遭受內存泄漏的影響呢?簡而言之,問題在於,Stack類本身管理內存(manage its own memory)、存儲池(storage pool)包含了elements數組(對象引用單元,而不是對象自己)的元素。數組活動區域(同前面的定義)中的元素是已分配的(allocated),而數組其他部分的元素則是自由的(free)。可是垃圾回收器沒法知道這一點;對於垃圾回收器而言,elements數組中的全部對象引用都同等有效。只有程序猿知道數組的非活動部分是不重要的。程序猿能夠把這個狀況告知垃圾回收器,作法很簡單:一旦數組元素變成了非活動部分的一部分,程序猿就手動清空這些數組元素。測試
一般來講,只要類是本身管理內存,程序猿就應該警戒內存泄漏問題。一旦元素被釋放掉,則該元素中包含的任何對象引用都應該被清空。spa
內存泄漏的另外一個常見來源是緩存。一旦你把對象引用放到緩存中,它就很容易被遺忘掉,從而使得它在很長一段時間沒有使用,可是卻仍然留在緩存中。對於這個問題,這裏有好幾種解決方案。若是你正好要實現這樣的緩存,只要在緩存以外存在對某個項的鍵的引用,該項就有意義,那麼就能夠用WeakHashMap表明緩存,當緩存中的項過時以後,它們就會自動被刪除。記住只有當所要的緩存項的生命週期是由該鍵的外部引用而不是由值決定時,WeakHashMap纔有用處。
更爲常見的情形則是,「緩存項的生命週期是否有意義」並非很容易肯定,隨着時間的推移,其中的項會變得愈來愈沒有價值。在這種狀況下,緩存應該時不時地清除掉沒用的項。這項清除工做能夠由一個後臺線程(多是Timer或者ScheduledThreadPoolExecutor)來完成,或者也能夠在給緩存添加新項的時候順便進行清理。LinkedHashMap類利用它的removeEldestEntry方法能夠很容易地實現後一種方案。對於更加複雜的緩存,必須直接使用java.lang.ref。
內存泄漏的第三個常見來源是監聽器和其餘回調。若是你實現了一個API,客戶端在這個API中註冊回調,卻沒有顯示地取消註冊,那麼除非你採起某些動做,不然他們就會積累下來。確保回調當即被當作垃圾回收的最佳方法是隻保存他們的弱引用(weak reference),例如,只將它們保存成WeakHashMap中的鍵。
因爲內存泄漏一般不會表現出明顯的失敗跡象,因此他們能夠在一個系統中存在不少年。每每只有經過仔細檢查代碼,或者藉助於Heap剖析工具(Heap Profiler)才能發現內存泄漏問題。所以,若是能在內存泄漏發生以前就知道如何預測此類問題,並阻止它們發生,那是最好不過的了。