摘要:具有垃圾收集功能的編程語言中的內存泄漏更恰當地稱為無意識的對象保留是隱蔽的。清空對象引用應該是一種例外,而不是一種規范行為。消除過期引用最好的方法是讓包含該引用的變量結束其生命周期。
??當你從手工管理內存的語言(比如C或者C++)轉換到具有垃圾回收功能的語言的時候,程序猿的工作就會變得更加容易,因為當你用完了對象之后,他們就會被自動回收。當你第一次經歷對象回收功能的時候,會覺得這簡直有點不可思議。這很容易給你留下這樣的印象,認為自己不再需要考慮內存管理的事情了,其實不然。
??考慮下面這個簡單的棧實現的例子:
// 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項)。無論如何測試,它都會成功地通過每一項測試,但是這個程序中隱藏著一個問題。簡而言之,改程序存在“內存泄漏”,由于垃圾收集器的活動增加或者內存占用增加,程序性能的降低會逐漸表現出來。在極端的情況下,這種內存泄漏會導致磁盤分頁(Disk Paging),甚至導致程序失敗并出現OutOfMemoryError,但這種失敗情形相對比較少見。
??那么,程序中哪里發生了內存泄漏呢?如果一個棧先是增長,然后再收縮,那么,從棧中彈出來的對象將不會被當做垃圾回收,即使使用棧的程序不再引用這些對象,它們也不會回收,因為,棧內部維護著對這些對象的過期引用(obsolete references),所謂的過期引用,是指永遠也不會再被解除的引用。在本例中,凡是在element數組的“活動部分”(active portion)之外的任何引用都是過期的。活動部分是指element中下標小于size的那些元素。
??具有垃圾收集功能的編程語言中的內存泄漏(更恰當地稱為無意識的對象保留)是隱蔽的。如果無意中保留了對象引用,則不僅將該對象從垃圾回收中排除,而且該對象引用的任何對象也是如此,依此類推。即使無意中保留了少量對象引用,也會阻止許多對象被垃圾回收器收集,對性能可能產生很大影響。
??這類問題的修復方法很簡單:一旦對象引用已經過期,只需要清空這些引用即可。對于上述例子中的Stack類而言,只要一個單元被彈出棧,指向它的引用就過期了,pop方法的修訂版本如下所示:
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; }
??清空過期引用的另一個好處是,如果它們以后又被錯誤地解除引用,程序就會立即拋出NullPointerException異常,而不是悄悄地錯誤運行下去。盡快檢測出程序中的錯誤總是有益的。
??當程序員第一次被類似這樣的問題困擾的時候,它們往往會過分小心:對于每一個對象的引用,一旦程序不再用到它,就把它清空。其實這樣做即沒必要,也不是我們所期望的,因為這樣做會把程序代碼弄得很亂。清空對象引用應該是一種例外,而不是一種規范行為。消除過期引用最好的方法是讓包含該引用的變量結束其生命周期。如果你是在最緊湊的作用域范圍內定義每一個變量(第57項),這種情形就會自然而然地發生。
??那么,何時應該清空引用呢?Stack類的哪方面特性使它易于遭受內存泄漏的影響呢?簡而言之,問題在于,Stack類自己管理內存(manage its own memory)、存儲池(storage pool)包含了elements數組(對象引用單元,而不是對象本身)的元素。數組活動區域(同前面的定義)中的元素是已分配的(allocated),而數組其余部分的元素則是自由的(free)。但是垃圾回收器無法知道這一點;對于垃圾回收器而言,elements數組中的所有對象引用都同等有效。只有程序猿知道數組的非活動部分是不重要的。程序猿可以把這個情況告知垃圾回收器,做法很簡單:一旦數組元素變成了非活動部分的一部分,程序猿就手動清空這些數組元素。
??通常來說,只要類是自己管理內存,程序猿就應該警惕內存泄漏問題。一旦元素被釋放掉,則該元素中包含的任何對象引用都應該被清空。
??內存泄漏的另一個常見來源是緩存。一旦你把對象引用放到緩存中,它就很容易被遺忘掉,從而使得它在很長一段時間沒有使用,但是卻仍然留在緩存中。對于這個問題,這里有好幾種解決方案。如果你正好要實現這樣的緩存,只要在緩存之外存在對某個項的鍵的引用,該項就有意義,那么就可以用WeakHashMap代表緩存,當緩存中的項過期之后,它們就會自動被刪除。記住只有當所要的緩存項的生命周期是由該鍵的外部引用而不是由值決定時,WeakHashMap才有用處。
??更為常見的情形則是,“緩存項的生命周期是否有意義”并不是很容易確定,隨著時間的推移,其中的項會變得越來越沒有價值。在這種情況下,緩存應該時不時地清除掉沒用的項。這項清除工作可以由一個后臺線程(可能是Timer或者ScheduledThreadPoolExecutor)來完成,或者也可以在給緩存添加新項的時候順便進行清理。LinkedHashMap類利用它的removeEldestEntry方法可以很容易地實現后一種方案。對于更加復雜的緩存,必須直接使用java.lang.ref。
??內存泄漏的第三個常見來源是監聽器和其他回調。如果你實現了一個API,客戶端在這個API中注冊回調,卻沒有顯示地取消注冊,那么除非你采取某些動作,否則他們就會積累下來。確保回調立即被當做垃圾回收的最佳方法是只保存他們的弱引用(weak reference),例如,只將它們保存成WeakHashMap中的鍵。
??由于內存泄漏通常不會表現出明顯的失敗跡象,所以他們可以在一個系統中存在很多年。往往只有通過仔細檢查代碼,或者借助于Heap剖析工具(Heap Profiler)才能發現內存泄漏問題。因此,如果能在內存泄漏發生之前就知道如何預測此類問題,并阻止它們發生,那是最好不過的了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/77468.html
摘要:一個類可以提供一個公共靜態工廠方法,它僅僅是一第項遇到多個構造器參數時要考慮使用構建器靜態工廠和構造器有個共同的局限性他們都不能很好地擴展到大量的可選參數。 ??本章涉及創建和銷毀對象,包括何時以及如何創建它們,何時以及如何避免創建它們,如何確保它們被及時銷毀,以及如何管理在銷毀之前必須進行的清理操作。 第1項:用靜態工廠方法代替構造器 ??類允許客戶端獲取實例的傳統方法是提供公共構造...
摘要:推薦序前言致謝第一章引言第二章創建和銷毀對象第項用靜態工廠方法代替構造器第項遇到多個構造器參數時要考慮使用構建器第項用私有構造器或者枚舉類型強化屬性第項通過私有構造器強化不可實例化的能力第項優先考慮依賴注入來引用資源第項避免創建不必要的對象 推薦序 前言 致謝 第一章 引言 第二章 創建和銷毀對象 第1項:用靜態工廠方法代替構造器 第2項:遇到多個構造器參數時要考慮使用構建器 第...
摘要:本章中的大部分內容適用于構造函數和方法。第項其他方法優先于序列化第項謹慎地實現接口第項考慮使用自定義的序列化形式第項保護性地編寫方法第項對于實例控制,枚舉類型優先于第項考慮用序列化代理代替序列化實例附錄與第版中項目的對應關系參考文獻 effective-java-third-edition 介紹 Effective Java 第三版全文翻譯,純屬個人業余翻譯,不合理的地方,望指正,感激...
閱讀 2905·2021-11-24 09:39
閱讀 1163·2021-11-02 14:38
閱讀 4156·2021-09-10 11:26
閱讀 2748·2021-08-25 09:40
閱讀 2310·2019-08-30 15:54
閱讀 482·2019-08-30 10:56
閱讀 2744·2019-08-26 12:14
閱讀 3216·2019-08-26 12:13