Effective Java 3rd 條目7 消除過(guò)期對(duì)象引用

如果你是從手動(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ā)生。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末兑障,一起剝皮案震驚了整個(gè)濱河市侄非,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌流译,老刑警劉巖逞怨,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異福澡,居然都是意外死亡叠赦,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門革砸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)除秀,“玉大人,你說(shuō)我怎么就攤上這事算利〔岵龋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵笔时,是天一觀的道長(zhǎng)棍好。 經(jīng)常有香客問(wèn)我仗岸,道長(zhǎng)允耿,這世上最難降的妖魔是什么借笙? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮较锡,結(jié)果婚禮上业稼,老公的妹妹穿的比我還像新娘。我一直安慰自己蚂蕴,他們只是感情好低散,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著骡楼,像睡著了一般熔号。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸟整,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天引镊,我揣著相機(jī)與錄音,去河邊找鬼篮条。 笑死弟头,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的涉茧。 我是一名探鬼主播赴恨,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼伴栓!你這毒婦竟也來(lái)了伦连?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤钳垮,失蹤者是張志新(化名)和其女友劉穎除师,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體扔枫,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡汛聚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了短荐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倚舀。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖忍宋,靈堂內(nèi)的尸體忽然破棺而出痕貌,到底是詐尸還是另有隱情,我是刑警寧澤糠排,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布舵稠,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏哺徊。R本人自食惡果不足惜室琢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望落追。 院中可真熱鬧盈滴,春花似錦、人聲如沸轿钠。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)疗垛。三九已至症汹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贷腕,已是汗流浹背烈菌。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留花履,地道東北人芽世。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像诡壁,于是被迫代替她去往敵國(guó)和親济瓢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容