6.Eliminate obsolete object reference
大意為 消除舊的對(duì)象引用
當(dāng)你使用直接操作內(nèi)存的語言,例如C或者C++的時(shí)候,一些內(nèi)存釋放的操作會(huì)比較麻煩,而我們使用java這一種擁有垃圾回收機(jī)制的語言的時(shí)候,這份工作就變得輕松多了,但是要注意的是,這個(gè)垃圾回收機(jī)制并不能讓我們對(duì)于內(nèi)存管理掉以輕心
考慮一下下面這個(gè)棧類型的實(shí)現(xiàn)
// 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);
}
}
看上去好像沒有什么明顯的問題,測(cè)試起來也都可以通過對(duì)吧,但是注意到其中潛伏的一個(gè)問題,寬泛點(diǎn)說,這個(gè)程序有著內(nèi)存泄漏的風(fēng)險(xiǎn),這樣的風(fēng)險(xiǎn)會(huì)導(dǎo)致垃圾回收的壓力增大并且加大內(nèi)存的開銷從而降低整個(gè)程序性能,最嚴(yán)重的時(shí)候可能會(huì)產(chǎn)生OutOfMemoryError的錯(cuò)誤,但是這樣的錯(cuò)誤比較少見
那么是那一部分內(nèi)存泄漏呢,其實(shí)就是pop彈出棧操作中stack仍然保留著已經(jīng)彈出的element的引用,那樣垃圾回收機(jī)制并不會(huì)去回收,并且這樣的一個(gè)舊的引用并不會(huì)被重引用,即使我們的stack沒有所有element的引用了,垃圾回收機(jī)制也不會(huì)去回收,由于stack一直維持著一個(gè)舊的引用
內(nèi)存泄漏在擁有垃圾回收機(jī)制(更加適合的說法是,無意的對(duì)象保留)的語言里面是十分陰險(xiǎn)的,如果一個(gè)對(duì)象的引用無意間保留了下來,不僅僅這個(gè)對(duì)象不會(huì)被垃圾回收,那些被這個(gè)對(duì)象所引用的對(duì)象也不能被回收,鏈?zhǔn)叫?yīng)會(huì)使得整個(gè)程序的性能極具下降
為了解決這樣的一個(gè)問題,我們只需要簡(jiǎn)單地把那些引用置為null就可以了,比如在上述程序中,我們只要這樣修改就好
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
只要你重引用這個(gè)元素,系統(tǒng)就會(huì)拋出一個(gè)空指針異常,這對(duì)于檢測(cè)程序異常錯(cuò)誤十分常見
當(dāng)程序員第一次面臨這個(gè)問題的時(shí)候,他們可能會(huì)在程序結(jié)束使用的時(shí)候過度去置空每一個(gè)對(duì)象的引用,這樣是既沒有必要又不被期望的,這樣會(huì)使得代碼變得雜亂,置空對(duì)象的引用應(yīng)該視情況而定而不是規(guī)范式地照搬,關(guān)于消除舊引用的最好的辦法就是讓這些包含引用的變量越界,這經(jīng)常在你在一個(gè)狹窄的范圍內(nèi)定義一個(gè)變量的時(shí)候會(huì)發(fā)生
那么,什么時(shí)候我們?nèi)ブ每者@些引用呢?簡(jiǎn)單來說,當(dāng)你的類中所擁有的變量的引用沒有重用的可能并且你的類還繼續(xù)擁有著這個(gè)引用的話,就把它置為空就可以了,如果不置為空,垃圾回收機(jī)制并不知道這個(gè)引用沒有作用了,也就不會(huì)去回收了
總而言之,只要當(dāng)一個(gè)類管理它自己的內(nèi)存,程序員就應(yīng)該注意一下內(nèi)存泄漏的風(fēng)險(xiǎn),當(dāng)一個(gè)元素是free的時(shí)候,任何對(duì)這個(gè)元素的引用都應(yīng)該被置空
另一個(gè)比較常見的可能造成內(nèi)存泄漏的原因就是緩存了,一旦你把一個(gè)對(duì)象的引用放到緩存里面,很容易忘記它在緩存那里并且很容易就把它一直放在緩存那里知道它變得完全沒有作用了,對(duì)于這類問題,有著一些解決方案,如果你足夠幸運(yùn),實(shí)現(xiàn)的緩存的條目都是完全相關(guān)的并且只要對(duì)于鍵值存在緩存外部的引用,代表性的緩存例如WeakHashMap,一旦條目變過時(shí)了就會(huì)自動(dòng)被移除,記住WeakHashMap,這個(gè)類很有用當(dāng)緩存條目的生存時(shí)間取決于外部對(duì)于鍵值的引用,而不是值的引用的時(shí)候
更加常見的是,一個(gè)緩存條目的有用的生存時(shí)間很少被定義的很好,隨著時(shí)間條目變得越來越?jīng)]有價(jià)值,在這種情況下,緩存應(yīng)該偶爾清一下那些不用了的條目,這可以利用后臺(tái)線程來處理(可能是一個(gè)Timer 或者是一個(gè)ScheduledThreadPoolExecutor )或者添加新的條目的時(shí)候就會(huì)有這種作用,LinkedHashMap類使用了它的removeEldestEntry方法使得后一種方法更加簡(jiǎn)便,對(duì)于更加復(fù)雜的緩存,你可能需要直接的使用 java.lang.ref
第三種常見的內(nèi)存泄漏的就是監(jiān)聽器和其他的回調(diào),如果你實(shí)現(xiàn)了一個(gè)API,這個(gè)API是當(dāng)用戶注冊(cè)回調(diào)但是并沒有明確的解除注冊(cè),他們會(huì)積累起來除非你采取某些措施,最好的辦法來保正這些回調(diào)被垃圾回收及時(shí)處理就是只儲(chǔ)存weak reference(弱引用),對(duì)于實(shí)例,就利用WeakHashMap儲(chǔ)存他們作為鍵
因?yàn)閮?nèi)存泄漏不會(huì)特別的明顯地顯示出來,他們可能在某個(gè)系統(tǒng)里面潛藏很久,只有十分仔細(xì)的代碼或者debug工具(比如heap profiler)的輔助才能發(fā)現(xiàn)他們,因此,我們需要學(xué)習(xí)去在這些問題發(fā)生之前預(yù)測(cè)并且防止他們發(fā)生