1. 程式人生 > >Effective Java 第三版讀書筆記——條款7:清除過期的物件引用

Effective Java 第三版讀書筆記——條款7:清除過期的物件引用

Java 帶有垃圾回收(garbage-collected)機制,這使程式設計師的工作變得容易了很多——因為你的物件在使用完畢以後就自動回收了。這很容易讓人覺得你不需要考慮記憶體管理,但這並不完全正確。

考慮下面這個簡單的棧實現:

// 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); } }

這個程式沒有什麼明顯的錯誤,但有一個潛在的問題——“記憶體洩漏”。由於垃圾回收器的活動的增加,或記憶體佔用的增加,程式的效能會下降。

那麼哪裡發生了記憶體洩漏? 如果一個棧增長後收縮,那麼從棧彈出的物件不會被當作垃圾回收掉,即使使用棧的程式不再引用這些物件。 這是因為棧維護了對這些物件的過期引用( 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;
}

當程式設計師第一次被這個問題困擾時,他們可能會在程式結束後立即清空所有物件引用。這既不是必要的,也不是可取的;它不必要地搞亂了程式。清空物件引用應該是例外而不是規範。消除過期引用的最好方法是讓包含引用的變數超出範圍。如果在最小的可能作用域內定義每個變數(條款 57),這種情況就會自然而然地發生。

下面是幾個常見的記憶體洩露來源:

  • 當一個類自己管理記憶體時。如上面的 Stack 類,陣列中活動部分的元素被分配,其餘的元素都是空閒的。垃圾收集器沒有辦法知道這些;對於垃圾收集器來說,elements 陣列中的所有物件引用都同樣有效。只有程式設計師知道陣列的非活動部分不重要。程式設計師可以向垃圾收集器傳達這樣一個事實:一旦陣列中的元素變成非活動的一部分,就可以手動清空這些元素的引用。

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

    更常見的情況是,快取項有用的生命週期不太明確,隨著時間的推移一些項變得越來越沒有價值。在這種情況下,快取應該偶爾清理掉已經廢棄的項。這可以通過一個後臺執行緒(也許是 ScheduledThreadPoolExecutor)來處理或將新的項新增到快取時順便清理。LinkedHashMap 類使用它的 removeEldestEntry 方法實現了後一種方案。對於更復雜的快取,可以直接使用java.lang.ref

  • 監聽器和其他回撥。如果你實現了一個API——其客戶端註冊回撥(callbacks),但是沒有顯式地撤銷他們的註冊。除非採取一些操作來處理,否則這些回撥會積累。確保回撥被垃圾回收的一種方法是隻儲存弱引用(weak references),例如,僅將它們儲存在 WeakHashMap 的鍵(key)中。