如果你是從手動(dòng)內(nèi)存管理的語(yǔ)言(比如C或者C++)切換到垃圾回收語(yǔ)言(比如Java)潜慎,作為程序員你的工作會(huì)變得更容易门岔,因?yàn)楫?dāng)你用完了對(duì)象時(shí)會(huì)被自動(dòng)回收砂沛。當(dāng)你第一次經(jīng)歷的時(shí)候,這好像是魔法一樣。這可能容易導(dǎo)致這種印象:你不必要考慮內(nèi)存管理,但是這不完全正確的奏窑。
考慮如下棧實(shí)現(xiàn)的例子:
// 你能指出“內(nèi)存泄漏”嗎?
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];
}
/**
* 保證至少有一個(gè)以上的元素的空間屈扎,每次隊(duì)列需要增長(zhǎng)時(shí)大約使容量加倍
* */
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
這個(gè)程序沒(méi)有明顯的錯(cuò)誤(但是參考條目29的泛型版本)埃唯。你可以徹底測(cè)試它,而且它會(huì)成功通過(guò)每項(xiàng)測(cè)試鹰晨,但是有個(gè)潛在的問(wèn)題墨叛。大約地講,這個(gè)程序有個(gè)“內(nèi)存泄漏”模蜡,當(dāng)由于垃圾回收器活動(dòng)增加和內(nèi)存占用增加而性能降低時(shí)漠趁,會(huì)悄悄的顯露出來(lái)。在極端情況下忍疾,這樣的內(nèi)存泄漏可以導(dǎo)致磁盤(pán)分頁(yè)闯传,甚至OutOfMemoryError的程序失敗,但是這樣的失敗是相當(dāng)稀少的卤妒。
那么內(nèi)存泄漏在哪里呢甥绿?如果棧增長(zhǎng)然后收縮叠必,從棧彈出的對(duì)象不會(huì)被垃圾回收,即使程序已經(jīng)沒(méi)有對(duì)它們的引用妹窖。這是因?yàn)闂>S持著對(duì)這些對(duì)象的過(guò)期引用(obsolete reference)纬朝。過(guò)期引用僅僅是一個(gè)沒(méi)有再次解引用的引用。從這個(gè)情況下骄呼,在元素隊(duì)列中的“有效區(qū)域”之外的任何引用都是過(guò)期的共苛。有效區(qū)域包含索引小于大小(size)的元素。
垃圾回收的語(yǔ)言中內(nèi)存泄漏(叫做無(wú)意對(duì)象留存(unintentional object retention)更合適)是潛隱的蜓萄。如果一個(gè)對(duì)象引用無(wú)意留存了隅茎,不僅是對(duì)象被垃圾回收排除之外,而且由這個(gè)對(duì)象引用的任何其他對(duì)象也是如此(諸如此類)嫉沽。即使只有一些對(duì)象引用無(wú)意存留辟犀,許許多多的對(duì)象也被阻止垃圾回收,這對(duì)性能可能有很大影響绸硕。
這種問(wèn)題的解決方案很簡(jiǎn)單:當(dāng)引用過(guò)期后置空堂竟。在我們的Stack類的情形中,對(duì)一個(gè)項(xiàng)的引用當(dāng)被彈出棧時(shí)玻佩,會(huì)成為過(guò)期出嘹。pop方法的糾正版本就像下面:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 消除過(guò)期引用
return result;
}
置空過(guò)期引用的額外好處是,如果后來(lái)它們被錯(cuò)誤地被解引用咬崔,這個(gè)程序立即以NullPointerException方式失敗税稼,而不是悄悄地做錯(cuò)誤的事情。盡快檢測(cè)到程序錯(cuò)誤垮斯,這總是有益的郎仆。
當(dāng)程序員第一次被這個(gè)問(wèn)題困擾時(shí),他們可能過(guò)度補(bǔ)償:一旦程序完成使用兜蠕,就置空每個(gè)對(duì)象引用扰肌。這既不必須也不合適;這不必要地把代碼凌亂了牺氨。置空對(duì)象引用應(yīng)該是特例而不是規(guī)范狡耻。消除過(guò)期引用的最佳方式是讓包含引用的變量掉出作用域墩剖。如果你在盡可能窄的域中定義每個(gè)變量猴凹,自然而然就會(huì)發(fā)生(條目57)。
那么你什么時(shí)候置空一個(gè)引用呢岭皂?Stack類的什么方面使得內(nèi)存泄漏容易呢郊霎?簡(jiǎn)單來(lái)講,它自己管理自己的內(nèi)存(manages its own memory)爷绘。存儲(chǔ)池(storage pool)包含元素隊(duì)列的元素(對(duì)象引用格(cell)书劝,而不是對(duì)象本身)进倍。隊(duì)列有效區(qū)域的元素是被分配的,而隊(duì)列其他的元素是空閑的购对。垃圾回收不會(huì)知道這些猾昆;對(duì)于垃圾回收器來(lái)說(shuō),元素隊(duì)列里面的所有對(duì)象引用是同樣有效骡苞。只有程序員知道隊(duì)列的無(wú)效部分是不重要的垂蜗。一旦隊(duì)列元素變?yōu)椴挥行Р糠值囊徊糠郑褪謩?dòng)地置空隊(duì)列元素解幽,通過(guò)這種方式贴见,程序員有效地和垃圾回收器溝通這個(gè)事實(shí)。
通常來(lái)說(shuō)躲株,無(wú)任何時(shí)一個(gè)類管理自己的內(nèi)存片部,程序員應(yīng)該警惕內(nèi)存泄漏。無(wú)任何時(shí)一個(gè)元素被釋放霜定,一個(gè)元素內(nèi)含的對(duì)象引用應(yīng)該被置空档悠。
另外一個(gè)內(nèi)存泄漏的來(lái)源是緩存。一旦你把對(duì)象引用放入到緩存時(shí)望浩,容易忘記它還在那里站粟,在它變得不相關(guān)的時(shí)候讓它久存在緩存中。這個(gè)問(wèn)題有幾個(gè)解決方案曾雕。只要在緩存之外鍵值對(duì)(entry)的的鍵有引用奴烙,鍵值對(duì)就有意義,如果你足夠幸運(yùn)實(shí)現(xiàn)這樣的緩存剖张,可以用WeakHashMap呈現(xiàn)這個(gè)緩存切诀;在鍵值對(duì)過(guò)期之后,它們可以自動(dòng)的被移除搔弄。記住幅虑,只有在緩存鍵值對(duì)的期望生命周期是由鍵的外部引用決定,而不是值的時(shí)候顾犹,WeakHashMap才是有用的倒庵。
更普遍地,隨著時(shí)間鍵值對(duì)變得越來(lái)越?jīng)]有價(jià)值炫刷,緩存鍵值對(duì)的有用生命周期也沒(méi)有很好的定義擎宝。在這些情況下,緩存應(yīng)該不定期清理不再用的鍵值對(duì)浑玛。這個(gè)可以用后臺(tái)線程(或許是一個(gè)ScheduledThreadPoolExecutor)完成绍申,或者可以作為添加新的鍵值對(duì)到緩存的副作用。通過(guò)它的removeEldestEntry方法,LinkedHashMap類使得后面這種方法更加容易极阅。對(duì)于更復(fù)雜的緩存胃碾,你可以直接用java.lang.ref。
第三個(gè)內(nèi)存泄漏的來(lái)源是監(jiān)聽(tīng)器(listener)和其他的回調(diào)(callback)筋搏。如果你實(shí)現(xiàn)了一個(gè)API仆百,客戶端可以注冊(cè)回調(diào),但是不會(huì)顯式地反注冊(cè)奔脐,除非你采取一些行動(dòng)儒旬,否則它們將積累。一個(gè)保證回調(diào)立即被垃圾回收的方式是帖族,只存儲(chǔ)對(duì)它們的虛引用栈源,比如,僅僅把他們作為WeakHashMap的鍵來(lái)存儲(chǔ)竖般。
因?yàn)閮?nèi)存泄漏通常不會(huì)像明顯的失敗來(lái)顯現(xiàn)甚垦,它們可能在一個(gè)系統(tǒng)中存在數(shù)年。它們通常僅僅通過(guò)仔細(xì)的代碼檢查涣雕,或者在調(diào)試工具(叫做heap profiler)的幫助下被發(fā)現(xiàn)艰亮。所以,這是非常值得的挣郭,學(xué)會(huì)在這樣的問(wèn)題發(fā)生之前預(yù)見(jiàn)到它們迄埃,并阻止它們發(fā)生。