整理Effective Java書中Item 7: Eliminate obsolete object references心得筆記

主旨

由於Java有GC的機制,很多時候開發者不會考慮到記憶體管理或著覺得不需要考慮,這是錯誤的觀念,如果忽略可能導致潛在memory leak問題,必須重視。

點出問題

  1. array
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

這個蠻經典的範例在ArrayList原始碼中,調用remove後,關鍵就在elementData[--size] = null;這行進行釋放,如果沒有這行GC不會知道要處理。

  1. caches

我們常會將很多不同的資料放到cache裡面,但過些時間後又沒有定期清理,很容易就把資料忘在裡面,即使已經不在使用了。如果是這總狀況可以使用WeakHashMap來當cache的map。存在這個map的值如果已經沒有任何外部引用將自動被刪除。這裡的Weak指的就是弱引用,表示若沒有任何引用當GC執行後就馬上就被清理。可以跑跑看下面範例。

public class Item7 {

    public static void main(String[] args) {

        Map<Key, Value> cache = new WeakHashMap<>();
        Key key1 = new Key("Alice");
        Value value1 = new Value("apple");
        Key key2 = new Key("Bob");
        Value value2 = new Value("orange");
        cache.put(key1, value1);
        cache.put(key2, value2);

        key1 = null;
        System.gc();

        for (Map.Entry<Key, Value> entry : cache.entrySet()) {
            System.out.println("Key : " + entry.getKey() + " Value : " + entry.getValue());
        }

    }

    static class Key {

        private String key;

        public Key(final String key) {
            this.key = key;
        }

        public String getKey() {
            return key;
        }

        public void setKey(String key) {
            this.key = key;
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("Key [key=");
            builder.append(key);
            builder.append("]");
            return builder.toString();
        }

    }

    static class Value {

        private String value;

        public Value(final String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("Value [value=");
            builder.append(value);
            builder.append("]");
            return builder.toString();
        }

    }
}
  1. listeners and other callbacks

假如你在總公司提供了一個簡單的推送API服務,集團會員登入上線會自動註冊你的服務,會員就可以收到總公司的推送消息。一開始沒什麼問題,使用時間久了後發現server負擔越來越大導致OOM,一時也找不出原因只能先靠重啟暫時解決問題,這樣類似的問題困擾著很多開發者又不得其門而入。這個例子其實很多會員下線了以後服務根本不知道,也沒有進行取消註冊服務,導致最後服務每次都要一直大量推送資料給沒有在線的會員。

小結

記憶體洩漏的問題通常不會表現得很明顯,都會在某個壓力峰值或著某個特別的操作爆發,它們可能在系統裡面是一顆多年的毒瘤,目前的解決方法不外乎是仔細的進行code review以及小規模的重構系統,或著透過分析工具來檢測系統。

參考延伸閱讀: