? ? ? 如果你是從一個需要手動內(nèi)存管理的程序員(c/c++)轉(zhuǎn)到一個由自動內(nèi)存管理的程序員(java)钧惧,你的工作會更加簡單酱床。因?yàn)槟阈枰膶ο髸詣釉偕?dāng)你想要使用它的時(shí)候。這看起來很魔幻當(dāng)你第一次體驗(yàn)自動內(nèi)存管理康栈,他很很容易給人一種你需要記住內(nèi)存管理這件事的印象述吸。但是,這肯定是不正確的扼劈!
? ?思考這樣一段棧實(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);
? ? }
}
? ? 這段程序完全沒有錯誤(但是看一看條目29中類的版本)驻啤。你應(yīng)該盡可能地測試一下它,它將順利的通過所有的測試荐吵,但是這里仍然有一個潛在的問題骑冗。寬泛地說,這個程序有一個“內(nèi)存泄漏”的問題先煎,他可能導(dǎo)致提高垃圾回收的行為而靜悄悄地減少性能贼涩,在極端的情況下,他會導(dǎo)致OutOfMemoryError薯蝎,但是這樣一個失敗是很少見的遥倦!
? ? ? 那這里真的有內(nèi)存泄漏嗎?如果一個棧增長然后又收縮占锯,這些本來被pop掉的對象仍然在棧中袒哥,他們沒有被垃圾回收清理掉,盡管這個程序使用的棧不會再使用這些對象消略。這是因?yàn)闂0愡@些對象的引用堡称,一個廢棄的引用簡單來說是沒有地方再引用它,在這個問題上疑俭,在任何活動部分之外的引用是廢棄的粮呢,而活動范圍則是在這個棧的size小的部分!
? ?內(nèi)存泄漏在垃圾回收語言(更是當(dāng)?shù)卣f是不關(guān)注對象保留)總是潛在的。如果一個對象引用不關(guān)注它是否保存钞艇,不只是這個對象被垃圾回收排除掉,同時(shí)也關(guān)注一個對象的引用等等豪硅。甚至吐過很少的對象引用不小心被保存類哩照,許多對象可能會由于高好費(fèi)的垃圾回收而無法創(chuàng)建!
? ?這個問題的固定解法很簡單:只要一個引用被標(biāo)記為nul就可以類懒浮。在這個棧的例子中飘弧,當(dāng)這個對象從棧中pop之后识藤,那么這個兌現(xiàn)個引用到這個對象就被廢棄!這個正確的pop方法的版本是這樣:
public Object pop() {
? ? ?if (size == 0)
? ? ? ? ? ?throw new EmptyStackException();?
? ? ?Object result = elements[--size];
? ? ?elements[size] = null; // Eliminate obsolete reference
? ? ?return result;
}
? ?一個賦予null值的額外的好處是這個廢棄的引用不會因?yàn)槭д`而被關(guān)聯(lián)到對象上次伶!這段程序?qū)ⅠR通過空指針異常失敗痴昧,而不是靜靜地保持錯誤。這樣做總是對盡可能地找到程序中的錯誤是有好處的冠王!
? ?當(dāng)程序員第一次被這種錯誤傷害赶撰,他們可能過度地將在每次使用完一個對象之后將每一個對象引用賦予Null值,但是這樣做既不必要柱彻,也不是令人滿意的豪娜!賦予Null值的對象引用應(yīng)該是一種例外而不是一種標(biāo)準(zhǔn)。最好的方式來排除一個廢棄的對象哟楷,是改變來控制引用不會調(diào)出一個范圍內(nèi)瘤载,這通常發(fā)生在當(dāng)你每次你在你將對象定義在一個最狹隘的范圍中(條目57)。
? ?所以什么時(shí)候你應(yīng)該將一個引用賦值為null呢卖擅?又是這個棧類的哪一個方面影響出內(nèi)存泄露呢鸣奔?簡單來說,這是因?yàn)槌徒祝瑮V袛?shù)組的內(nèi)存是由它自己管理溃蔫!這個存儲池由一個有多個element的數(shù)組組成,這些element在數(shù)組被分配的活動范圍之內(nèi)琳猫,然后剩余的數(shù)組部分就是空的伟叛。但是垃圾回收不知道這些事情!對于垃圾回收器來說脐嫂,數(shù)組中所有的element都是相等的统刮,只有程序員才知道哪些活動范圍內(nèi)的element是不重要的。所以程序員需要在這數(shù)組中的element變得不重要了就要將他們賦予null值账千。
? 通常來說侥蒙,無論是不是一個類管理它自己的內(nèi)存,程序員都應(yīng)該注意到內(nèi)存泄露的問題匀奏。無論什么時(shí)候鞭衩,一個element不會再使用了,所有包含他的對象引用都應(yīng)該被賦予null值娃善。
? ?? 另一個常見的內(nèi)存泄露是緩存论衍。一旦你放了一個對象的引用到一個緩存,很容易就會忘記它在緩存中而導(dǎo)致這個對象在沒有用之后還長時(shí)間留在緩存中聚磺。這里對這個問題有幾個方案坯台。如果你很幸運(yùn)可以去實(shí)現(xiàn)一個緩存,你確定這個緩存會有一些緩存在廢棄之后還會存在緩存之內(nèi)瘫寝,使用WeakHashMap來實(shí)現(xiàn)這個緩存蜒蕾。這些Map中的實(shí)體將會在廢棄之后被自動移除稠炬。記住這個WeakHashMap將會非常有用,但是僅當(dāng)確定這個緩存的鍵可能會廢棄咪啡,而Map中的值首启。
? ? ?更常見的是,這個緩存變得沒有價(jià)值之前的使用時(shí)間很少被定義撤摸,在某些狀況之下毅桃,可能偶爾會很清楚地知道一個緩存的實(shí)體已經(jīng)變得不可用了。這個可能會是在后臺線程需要做的工作愁溜。LinkedHashMap使用它的removeEldestEntry 提供了一個的方案疾嗅。但是,對于很多復(fù)雜的緩存你可能需要首先使用java.lang.ref來解決問題冕象。
? ? 第三個可能內(nèi)存泄露的地方是監(jiān)聽器和回調(diào)函數(shù)代承,如果你實(shí)現(xiàn)了這個客戶端注冊回調(diào)函數(shù)的API,但是沒有明確地將他們撤銷注冊渐扮,他們將會堆砌起來除非你對它們做一些事论悴。一個方式是敏捷地確認(rèn)這些回調(diào)函數(shù)已經(jīng)被垃圾回收器標(biāo)記為弱引用。例如對于實(shí)例墓律,將他們以鍵的形式存儲在WeakHashMap膀估。因?yàn)閮?nèi)存泄露通常不會因?yàn)橥耆某绦虺鲥e而表現(xiàn)出來,他們通常會存在一個系統(tǒng)幾年耻讽,它們通常只會在仔細(xì)的代碼檢查中或者借用有機(jī)制的代碼調(diào)試工具(比如說heap profiler)才會被發(fā)現(xiàn)察纯。因此,在這種問題出現(xiàn)之前預(yù)先去學(xué)習(xí)知道這個問題通常是很讓人滿意的针肥!