Effective Java(3rd)-Item7:消除過(guò)時(shí)的對(duì)象引用

??如果你從手動(dòng)管理內(nèi)存的編程語(yǔ)言如C和C++切換到垃圾回收語(yǔ)言比如Java,作為程序員,你的工作會(huì)變得更容易,因?yàn)槟愕膶?duì)象會(huì)被自動(dòng)回收。在你第一次實(shí)踐時(shí),你會(huì)感覺(jué)這仿佛就是魔法。它很容易導(dǎo)致你誤認(rèn)為你不必考慮內(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);
    }
}

??這個(gè)程序沒(méi)有明顯的錯(cuò)誤(但是參見(jiàn)條目29的通用版本)。你可以詳盡地測(cè)試它,并且它將通過(guò)每個(gè)測(cè)試,但是它有著一個(gè)潛在的問(wèn)題。簡(jiǎn)而言之,該程序存在“內(nèi)存泄漏”,由于垃圾收集器活動(dòng)的增加或內(nèi)存占用的增加導(dǎo)致該程序性能下降。在極度情況下,這樣的內(nèi)存泄露可能導(dǎo)致磁盤(pán)分頁(yè)甚至程序失敗并出現(xiàn)OutOfMemoryError,但是這樣的故障相對(duì)較少。
??所以,哪里內(nèi)存泄漏了?如果棧增長(zhǎng)然后收縮,被棧彈出去的對(duì)象將不會(huì)被垃圾回收器給回收,即使使用這個(gè)棧的程序?qū)λ鼈儧](méi)有更多的引用。這是因?yàn)闂H耘f維護(hù)著對(duì)這些對(duì)象的過(guò)時(shí)引用。過(guò)時(shí)引用是一個(gè)永遠(yuǎn)不會(huì)再被解除引用的引用。在這種情況下,元素?cái)?shù)組的“活動(dòng)部分”以外的任何引用都是過(guò)時(shí)的。活動(dòng)部分由索引小于大小的元素組成。
??垃圾收集語(yǔ)言中的內(nèi)存泄漏(更恰當(dāng)?shù)胤Q(chēng)為無(wú)意識(shí)的對(duì)象保留)是隱秘的。如果一個(gè)對(duì)象引用被無(wú)意中保留,不僅該對(duì)象被垃圾回收排除,而且任何被該對(duì)象引用的對(duì)象也被排除,以此類(lèi)推。即使無(wú)意中保留了少量的對(duì)象引用,也會(huì)阻止大量對(duì)象被垃圾回收,潛在地大量影響性能。
??解決這類(lèi)問(wèn)題的方法很簡(jiǎn)單:一旦它們變得過(guò)時(shí),將它們指向null。在我們的例子Stack類(lèi)的情況下,當(dāng)項(xiàng)目被棧彈出時(shí),它的引用就過(guò)時(shí)了。pop方法的更正版本如下:

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

??將過(guò)時(shí)引用置null的另一個(gè)好處是如果它們隨后被錯(cuò)誤地取消引用,程序?qū)⒘⒓词〔伋鯪ullPointerException,而不是安靜地執(zhí)行錯(cuò)誤操作。盡可能快地檢測(cè)編程錯(cuò)誤總是有益的。當(dāng)程序員第一次被這個(gè)問(wèn)題所困擾時(shí),他們可能在程序使用完后盡快清空每個(gè)對(duì)象引用而過(guò)度補(bǔ)償。這既沒(méi)有必要也不可??;它不必要地混亂了程序。取消對(duì)象引用應(yīng)該是例外而不是規(guī)范。最好的方式來(lái)清除過(guò)時(shí)引用就是讓包含引用的變量超出范圍。如果你在盡可能窄的范圍內(nèi)定義每個(gè)變量,這自然會(huì)發(fā)生(item57)
??所以什么時(shí)候你應(yīng)該將引用置空?Stack類(lèi)的什么方面容易導(dǎo)致內(nèi)存泄漏?簡(jiǎn)單地說(shuō),當(dāng)它自己管理自己的內(nèi)存的時(shí)候。存儲(chǔ)池由元素?cái)?shù)組的元素組成(對(duì)象引用單元格而不是對(duì)象本身),數(shù)組(如前所述)活動(dòng)部分中的元素被分配,數(shù)組中的其余部分是空閑的。垃圾回收器沒(méi)有辦法知道這一點(diǎn);對(duì)于垃圾回收器來(lái)說(shuō),所有在元素?cái)?shù)組中的對(duì)象引用都同樣有效。只有程序員知道數(shù)組的不活動(dòng)部分是不重要的。程序員通過(guò)在數(shù)組元素成為非活動(dòng)部分時(shí)立即手動(dòng)清空元素?cái)?shù)組,有效地傳遞這個(gè)信息給垃圾回收器。
??總體上說(shuō),只要一個(gè)類(lèi)管理了它自己的內(nèi)存,程序員就該警惕內(nèi)存泄漏,只要一個(gè)元素被釋放,任何包含這個(gè)元素的對(duì)象引用就該被清除掉。
??另一個(gè)內(nèi)存泄漏的常見(jiàn)原因是緩存。當(dāng)你將對(duì)象引用放入緩存后,很容易忘記它在那里,并且它變得無(wú)關(guān)緊要后就一直保留在緩存里。這種問(wèn)題由幾種解決辦法。如果你足夠幸運(yùn)來(lái)實(shí)現(xiàn)一個(gè)條目相關(guān)的緩存,只要在緩存之外有對(duì)其key的引用,就將緩存表示為WeakHashMap(https://www.cnblogs.com/skywang12345/p/3311092.html)。條目將在過(guò)時(shí)后被自動(dòng)刪除。請(qǐng)記住,只有當(dāng)緩存條目的所需生存期的key被外部引用 時(shí)WeakHashMap才有用,而不是值。
??更常見(jiàn)的是,緩存條目的有用生存周期很難定義,隨著時(shí)間的推移條目變得不那么有價(jià)值。在這種情況下,緩存應(yīng)該偶爾清除那些已經(jīng)廢棄的條目。這可以通過(guò)后臺(tái)線(xiàn)程(可能是ScheduledThreadPoolExecutor)或添加新條目到緩存中的副作用來(lái)完成。LinkedHashMap使用removeEldestEntry方法促進(jìn)后一種方法。對(duì)于更復(fù)雜的緩存,你可能需要直接使用java.lang.ref(https://www.ibm.com/developerworks/cn/java/j-lo-langref/index.html) 。
??內(nèi)存泄漏的第三種普遍來(lái)源是監(jiān)聽(tīng)器和其他回調(diào)。如果你實(shí)現(xiàn)了一個(gè)API,客戶(hù)端注冊(cè)了回調(diào),但是并未明確注銷(xiāo)回調(diào),除非你采取一些措施,否則它們將累積。確?;卣{(diào)函數(shù)被及時(shí)垃圾回收的一個(gè)辦法是只是存儲(chǔ)它們的弱引用,例如,僅存儲(chǔ)它們作為WeakHashMap的鍵(key)。
??因?yàn)橥ǔ?nèi)存泄漏并不會(huì)表現(xiàn)為明顯的故障,它們可能會(huì)在系統(tǒng)中保留多年。它們通常只有當(dāng)仔細(xì)的代碼檢查或借助稱(chēng)為堆分析器的調(diào)試工具的情況下被發(fā)現(xiàn)。所以,非常需要學(xué)會(huì)預(yù)測(cè)這些問(wèn)題,并防止它們發(fā)生。
本文寫(xiě)于2019.1.11,歷時(shí)3天

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容