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


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

Effective Java, Third Edition

7. 消除過期的對象引用

如果你從使用手動內存管理的語言(如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)。 過期引用簡單來說就是永遠不會解除的引用。 在這種情況下,元素數組“活動部分(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數組中的所有對象引用都同樣有效。只有程序員知道數組的非活動部分不重要。程序員可以向垃圾收集器傳達這樣一個事實,一旦數組中的元素變成非活動的一部分,就可以手動清空這些元素的引用。

一般來說,當一個類自己管理內存時,程序員應該警惕內存泄漏問題。 每當一個元素被釋放時,元素中包含的任何對象引用都應該被清除。

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

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

第三個常見的內存泄漏來源是監聽器和其他回調。如果你實現了一個API,其客戶端注冊回調,但是沒有顯式地撤銷注冊回調,除非采取一些操作,否則它們將會累積。確保回調是垃圾收集的一種方法是只存儲弱引用(weak references),例如,僅將它們保存在WeakHashMap的鍵(key)中。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM