內存泄漏的第一個常見來源是存在過期引用。
1 import java.util.Arrays; 2 import java.util.EmptyStackException; 3 4 public class Stack { 5 6 private Object[] elements; 7 private int size = 0; 8 private static final int DEFAULT_INITIAL_CAPACITY = 16; 9 10 public Stack() { 11 elements = new Object[DEFAULT_INITIAL_CAPACITY]; 12 } 13 14 private void ensureCapacity() { 15 if (elements.length == size) { 16 elements = Arrays.copyOf(elements, 2 * size + 1); 17 } 18 } 19 20 public void push(Object e) { 21 ensureCapacity(); 22 elements[size++] = e; 23 } 24 25 public Object pop() { 26 if (size == 0) { 27 throw new EmptyStackException(); 28 } 29 30 return elements[--size]; 31 } 32 33 }
如果一個棧先是增長,然后再收縮,從棧中彈出來的對象不會被當作垃圾回收,即使使用棧的程序不再引用這些對象,它們也不會被回收。因為棧內部維護着對這些對象的過期引用(obsolete reference)。過期引用指永遠也不會再被解除的引用。在本例中,在elements數組的“活動部分(active portion)”之外的任何引用都是過期的。活動部分指elements中下標小於size的那些元素。
如果一個對象引用被無意識地保留了,垃圾回收機制不僅不會回收這個對象,而且不會回收被這個對象所引用的所有其他對象。解決方法:一旦對象引用已經過期,只需清空這些引用即可。在本例中,只要一個元素被彈出棧,指向它的引用就過期了。修改如下:
1 public Object pop() { 2 if (size == 0) { 3 throw new EmptyStackException(); 4 } 5 6 Object result = elements[--size]; 7 elements[size] = null; // 清空引用 8 9 return result; 10 }
清空過期引用的另一個好處是,如果它們以后又被錯誤地解除引用,程序會立即拋出NullPointerException異常,而不是悄悄地錯誤運行下去。盡快地檢測出程序中的錯誤總是有益的。消除過期引用最好的方法是讓包含該引用的變量結束其生命周期。如果在最緊湊的作用域范圍內定義每一個變量(見第45條),這種情形會自然而然地發生。
只要類是自己管理內存,程序員就應該警惕內存泄漏問題。一旦元素被釋放掉,該元素中包含的任何對象引用都應該被清空。
內存泄漏的第二個常見來源是緩存。
把對象引用放到緩存中,它就很容易被遺忘掉,從而使得它不再有用之后很長一段時間內仍然留在緩存中。用WeakHashMap代表緩存時,當緩存中的項過期之后,它們會被自動地刪除。只有當緩存項的生命周期是由該鍵的外部引用而不是由值決定時,WeakHashMap才起作用。
隨着時間的推移,緩存項會變得越來越沒有價值,緩存應該時不時地清除掉沒用的項。可以由一個后台線程(可能是Timer或者ScheduledThreadPoolExecutor)來完成,或者在給緩存添加新條目時進行清理。LinkedHashMap類可以通過它的removeEldestEntry方法來實現后者。對於更加復雜的緩存,必須直接使用java.lang.ref。
內存泄漏的第三個常見來源是監聽器和其他回調。
如果客戶端在自己實現的API中注冊回調,卻沒有顯式地取消注冊,那么除非自己采取某些動作,否則它們就會積聚。確保回調立即被當作垃圾回收的最佳方法是只保存它們的弱引用(weak reference),例如,只將它們保存為WeakHashMap中的鍵。
往往只有通過仔細檢查代碼,或者借助於Heap剖析工具(Heap Profiler)才能發現內存泄露問題。
參考資料
《Effective Java 中文版 第2版》 第6條:消除過期的對象引用 P21-23