Effective.Java 讀書筆記(6)內存泄漏

6.Eliminate obsolete object reference
大意爲 消除舊的對象引用

當你使用直接操作內存的語言,例如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);
}
}

看上去好像沒有什麼明顯的問題,測試起來也都可以通過對吧,但是注意到其中潛伏的一個問題,寬泛點說,這個程序有着內存泄漏的風險,這樣的風險會導致垃圾回收的壓力增大並且加大內存的開銷從而降低整個程序性能,最嚴重的時候可能會產生OutOfMemoryError的錯誤,但是這樣的錯誤比較少見

那麼是那一部分內存泄漏呢,其實就是pop彈出棧操作中stack仍然保留着已經彈出的element的引用,那樣垃圾回收機制並不會去回收,並且這樣的一箇舊的引用並不會被重引用,即使我們的stack沒有所有element的引用了,垃圾回收機制也不會去回收,由於stack一直維持着一箇舊的引用

內存泄漏在擁有垃圾回收機制(更加適合的說法是,無意的對象保留)的語言裏面是十分陰險的,如果一個對象的引用無意間保留了下來,不僅僅這個對象不會被垃圾回收,那些被這個對象所引用的對象也不能被回收,鏈式效應會使得整個程序的性能極具下降

爲了解決這樣的一個問題,我們只需要簡單地把那些引用置爲null就可以了,比如在上述程序中,我們只要這樣修改就好

public Object pop() {
if (size == 0)
     throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}

只要你重引用這個元素,系統就會拋出一個空指針異常,這對於檢測程序異常錯誤十分常見

當程序員第一次面臨這個問題的時候,他們可能會在程序結束使用的時候過度去置空每一個對象的引用,這樣是既沒有必要又不被期望的,這樣會使得代碼變得雜亂,置空對象的引用應該視情況而定而不是規範式地照搬,關於消除舊引用的最好的辦法就是讓這些包含引用的變量越界,這經常在你在一個狹窄的範圍內定義一個變量的時候會發生

那麼,什麼時候我們去置空這些引用呢?簡單來說,當你的類中所擁有的變量的引用沒有重用的可能並且你的類還繼續擁有着這個引用的話,就把它置爲空就可以了,如果不置爲空,垃圾回收機制並不知道這個引用沒有作用了,也就不會去回收了

總而言之,只要當一個類管理它自己的內存,程序員就應該注意一下內存泄漏的風險,當一個元素是free的時候,任何對這個元素的引用都應該被置空

另一個比較常見的可能造成內存泄漏的原因就是緩存了,一旦你把一個對象的引用放到緩存裏面,很容易忘記它在緩存那裏並且很容易就把它一直放在緩存那裏知道它變得完全沒有作用了,對於這類問題,有着一些解決方案,如果你足夠幸運,實現的緩存的條目都是完全相關的並且只要對於鍵值存在緩存外部的引用,代表性的緩存例如WeakHashMap,一旦條目變過時了就會自動被移除,記住WeakHashMap,這個類很有用當緩存條目的生存時間取決於外部對於鍵值的引用,而不是值的引用的時候

更加常見的是,一個緩存條目的有用的生存時間很少被定義的很好,隨着時間條目變得越來越沒有價值,在這種情況下,緩存應該偶爾清一下那些不用了的條目,這可以利用後臺線程來處理(可能是一個Timer 或者是一個ScheduledThreadPoolExecutor )或者添加新的條目的時候就會有這種作用,LinkedHashMap類使用了它的removeEldestEntry方法使得後一種方法更加簡便,對於更加複雜的緩存,你可能需要直接的使用 java.lang.ref

第三種常見的內存泄漏的就是監聽器和其他的回調,如果你實現了一個API,這個API是當用戶註冊回調但是並沒有明確的解除註冊,他們會積累起來除非你採取某些措施,最好的辦法來保正這些回調被垃圾回收及時處理就是隻儲存weak reference(弱引用),對於實例,就利用WeakHashMap儲存他們作爲鍵

因爲內存泄漏不會特別的明顯地顯示出來,他們可能在某個系統裏面潛藏很久,只有十分仔細的代碼或者debug工具(比如heap profiler)的輔助才能發現他們,因此,我們需要學習去在這些問題發生之前預測並且防止他們發生









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章