Effective Java 第三版——7. 消除過時的對象引用

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。java

Effective Java, Third Edition

7. 消除過時的對象引用

若是你從使用手動內存管理的語言(如C或c++)切換到像Java這樣的帶有垃圾收集機制的語言,那麼做爲程序員的工做就會變得容易多了,由於你的對象在使用完畢之後就自動回收了。當你第一次體驗它的時候,它就像魔法同樣。這很容易讓人以爲你不須要考慮內存管理,但這並不徹底正確。c++

考慮如下簡單的堆棧實現:程序員

// 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)。 過時引用簡單來講就是永遠不會解除的引用。 在這種狀況下,元素數組「活動部分(active portion)」以外的任何引用都是過時的。 活動部分是由索引下標小於size的元素組成。緩存

垃圾收集語言中的內存泄漏(更適當地稱爲無心的對象保留 unintentional object retentions)是隱蔽的。 若是無心中保留了對象引用,那麼不只這個對象排除在垃圾回收以外,並且該對象引用的任何對象也是如此。 即便只有少數對象引用被無心地保留下來,也能夠阻止垃圾回收機制對許多對象的回收,這對性能產生很大的影響。工具

這類問題的解決方法很簡單:一旦對象引用過時,將它們設置爲 null。 在咱們的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類的哪一個方面使它容易受到內存泄漏的影響?簡單地說,它管理本身的內存。存儲池(storage pool)由elements數組的元素組成(對象引用單元,而不是對象自己)。數組中活動部分的元素(如前面定義的)被分配,其他的元素都是空閒的。垃圾收集器沒有辦法知道這些;對於垃圾收集器來講,elements數組中的全部對象引用都一樣有效。只有程序員知道數組的非活動部分不重要。程序員能夠向垃圾收集器傳達這樣一個事實,一旦數組中的元素變成非活動的一部分,就能夠手動清空這些元素的引用。spa

通常來講,當一個類本身管理內存時,程序員應該警戒內存泄漏問題。 每當一個元素被釋放時,元素中包含的任何對象引用都應該被清除。

另外一個常見的內存泄漏來源是緩存。一旦將對象引用放入緩存中,很容易忘記它的存在,而且在它變得可有可無以後,仍然保留在緩存中。對於這個問題有幾種解決方案。若是你正好想實現了一個緩存:只要在緩存以外存在對某個項(entry)的鍵(key)引用,那麼這項就是明確有關聯的,就能夠用WeakHashMap來表示緩存;這些項在過時以後自動刪除。記住,只有當緩存中某個項的生命週期是由外部引用到鍵(key)而不是值(value)決定時,WeakHashMap纔有用。

更常見的狀況是,緩存項有用的生命週期不太明確,隨着時間的推移一些項變得愈來愈沒有價值。在這種狀況下,緩存應該偶爾清理掉已經廢棄的項。這能夠經過一個後臺線程(也許是ScheduledThreadPoolExecutor)或將新的項添加到緩存時順便清理。LinkedHashMap類使用它的removeEldestEntry方法實現了後一種方案。對於更復雜的緩存,可能直接須要使用java.lang.ref

第三個常見的內存泄漏來源是監聽器和其餘回調。若是你實現了一個API,其客戶端註冊回調,可是沒有顯式地撤銷註冊回調,除非採起一些操做,不然它們將會累積。確保回調是垃圾收集的一種方法是隻存儲弱引用(weak references),例如,僅將它們保存在WeakHashMap的鍵(key)中。

由於內存泄漏一般不會表現爲明顯的故障,因此它們可能會在系統中保持多年。 一般僅在仔細的代碼檢查或藉助堆分析器( heap profiler)的調試工具纔會被發現。 所以,學習如何預見這些問題,並防止這些問題發生,是很是值得的。

相關文章
相關標籤/搜索