文章出處

我記得在有一次面試中,面試官問我自己實現的一個棧中會不會有內存泄露的問題,我努力搜索可能的問題,就是感受不到可能出現的問題。當時忽然意識到,內存泄露這個問題一直被我忽略,因為用的是java/C#,這些語言中都有內存自動回收的機制,我突然發現自己對這個問題竟然一無所知。面試中的棧就是下面這個:

// 你能檢查出"內存泄露"嗎?
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];
    }

    /**
     * 保證棧能自動增長,當棧中空間不足時,自動增長為原長度的兩倍
     */
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

這段程序不管你怎么測試都是沒有問題的,但是他確實可能引起“內存泄露”。定位到pop()函數,在return語句中,當我們彈出一個元素時,只是簡單的讓棧頂指針(size)-1。邏輯上,棧中的這個元素已經彈出,已經沒有用了。但是事實上,被彈出的元素依然存在于elements數組中,它依然被elements數組所引用,GC是無法回收被引用著的對象的。也許你期望等這整個棧失去引用(將被GC回收時),棧內的elements數組一起被GC回收。但是實際的使用過程中,又有誰能夠預料到這個棧會存活多長時間。為了保險起見,我們需要在彈出一個元素的時候,就讓這個元素失去引用,便于GC回收。我們只需要讓Pop()函數彈出時,同時解除對彈出元素的引用即可。

public Object pop() {
if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // 消除過期的引用
        return result;
    }

從上面的例子中,我們可以發現當類中持有過期的元素的引用時,就有可能造成內存泄露問題。而且通常這種內存泄露問題都是我們無意識造成的,上面的棧中,邏輯上我們認為彈出的元素就應該被GC回收掉,但事實上GC沒有辦法回收,因此elements數組依然持有它。這種問題很隱蔽,通常只要類自己管理內存(如類中有一個Array或List型的結構),那么我們就應該警惕內存泄露的問題。

 

內存泄露來源及解決

    內存泄露可能來源于緩存。我們為了讓下次的程序的處理速度更快,常常需要將一些信息緩存在內存中,但是這些過期的緩存又很容易被遺忘,從而使得它不再有用之后很長一段時間內仍然留在緩存中。例如像一個要顯示圖片墻的程序,我們需要緩存圖片和相關的信息,為了方便GC回收過期的緩存,我們可以使用WeakHashMap來實現緩存,當界面顯示圖片的時候,界面持有相關圖片的引用,這些引用同時也存在于WeakHashMap中。而其他不被界面持有的過期緩存,則WeakHashMap會自動將這些剔除。

    總的說來,只要在緩存之外存在對某個項的鍵的引用,該項就有意義,那么就可以用WeakHashMap代表緩存;當緩存中的項過期之后,它們就自動被刪除。記住只有當所有的緩存項的生命周期是由該鍵的外部引用而不是由值決定時,WeakHashMap才有用處。

    更為常見的情景則是,"緩存項的生命周期是否有意義"并不是非常容易確定,隨著時間推移,其中的項會變得越來越沒有價值。在這種情況下,緩存應該是不是地清除掉沒有的項。這項清除工作可以由一個后臺線程(可能是Timer或者ScheduledThreadPoolExecutor)來完成,或者也可以在給緩存添加新條目的時候順便進行清理。LinkedHashMap類利用它的removeEldestEntry方法可以很容易地實現后一種方案。對于更加復雜的緩存,必須直接使用java.lang.ref.
    內存泄露的第三個常見來源是監聽器和其他回調。如果你實現了一個API,客戶端在這個API中注冊回調,卻沒有顯式地取消注冊,那么除非你采取某些動作,否則他們就會積聚。確保回調立即被當成垃圾回收的最佳方法是只保存它們的弱引用(weak reference),例如,只將它們保存成WeakHashMap中的鍵。

 

部分文字直接截取自《Effective Java》


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()