摘要
雖然java虛擬機(JVM)及其垃圾收集器(garbage collector,GC)負責治理大多數的內存任務,Java軟件程序中還是有可能出現內存泄漏。實際上,這在大型項目中是一個常見的問題。避免內存泄漏的第一步是要弄清楚它是如何發生的。本文介紹了編寫Java代碼的一些常見的內存泄漏陷阱,以及編寫不泄漏代碼的一些最佳實踐。一旦發生了內存泄漏,要指出造成泄漏的代碼是非常困難的。因此本文還介紹了一種新工具,用來診斷泄漏并指出根本原因。該工具的開銷非常小,因此可以使用它來尋找處于生產中的系統的內存泄漏。
垃圾收集器的作用
雖然垃圾收集器處理了大多數內存治理問題,從而使編程人員的生活變得更輕松了,但是編程人員還是可能犯錯而導致出現內存問題。簡單地說,GC循環地跟蹤所有來自“根”對象(堆棧對象、靜態對象、JNI句柄指向的對象,諸如此類)的引用,并將所有它所能到達的對象標記為活動的。程序只可以操縱這些對象;其他的對象都被刪除了。因為GC使程序不可能到達已被刪除的對象,這么做就是安全的。
雖然內存治理可以說是自動化的,但是這并不能使編程人員免受思考內存治理問題之苦。例如,分配(以及釋放)內存總會有開銷,雖然這種開銷對編程人員來說是不可見的。創建了太多對象的程序將會比完成同樣的功能而創建的對象卻比較少的程序更慢一些(在其他條件相同的情況下)。
而且,與本文更為密切相關的是,假如忘記“釋放”先前分配的內存,就可能造成內存泄漏。假如程序保留對永遠不再使用的對象的引用,這些對象將會占用并耗盡內存,這是因為自動化的垃圾收集器無法證實這些對象將不再使用。正如我們先前所說的,假如存在一個對對象的引用,對象就被定義為活動的,因此不能刪除。為了確保能回收對象占用的內存,編程人員必須確保該對象不能到達。這通常是通過將對象字段設置為null或者從集合(collection)中移除對象而完成的。但是,注重,當局部變量不再使用時,沒有必要將其顯式地設置為null。對這些變量的引用將隨著方法的退出而自動清除。
概括地說,這就是內存托管語言中的內存泄漏產生的主要原因:保留下來卻永遠不再使用的對象引用。
典型泄漏
既然我們知道了在Java中確實有可能發生內存泄漏,就讓我們來看一些典型的內存泄漏及其原因。
全局集合
在大的應用程序中有某種全局的數據儲存庫是很常見的,例如一個JNDI樹或一個會話表。在這些情況下,必須注重治理儲存庫的大小。必須有某種機制從儲存庫中移除不再需要的數據。
這可能有多種方法,但是最常見的一種是周期性運行的某種清除任務。該任務將驗證儲存庫中的數據,并移除任何不再需要的數據。
另一種治理儲存庫的方法是使用反向鏈接(referrer)計數。然后集合負責統計集合中每個入口的反向鏈接的數目。這要求反向鏈接告訴集合何時會退出入口。當反向鏈接數目為零時,該元素就可以從集合中移除了。
緩存
緩存是一種數據結構,用于快速查找已經執行的操作的結果。因此,假如一個操作執行起來很慢,對于常用的輸入數據,就可以將操作的結果緩存,并在下次調用該操作時使用緩存的數據。
緩存通常都是以動態方式實現的,其中新的結果是在執行時添加到緩存中的。典型的算法是:
該算法的問題(或者說是潛在的內存泄漏)出在最后一步。假如調用該操作時有相當多的不同輸入,就將有相當多的結果存儲在緩存中。很明顯這不是正確的方法。
為了預防這種具有潛在破壞性的設計,程序必須確保對于緩存所使用的內存容量有一個上限。因此,更好的算法是:
通過始終移除緩存最久的結果,我們實際上進行了這樣的假設:在將來,比起緩存最久的數據,最近輸入的數據更有可能用到。這通常是一個不錯的假設。
新算法將確保緩存的容量處于預定義的內存范圍之內。確切的范圍可能很難計算,因為緩存中的對象在不斷變化,而且它們的引用包羅萬象。為緩存設置正確的大小是一項非常復雜的任務,需要將所使用的內存容量與檢索數據的速度加以平衡。
解決這個問題的另一種方法是使用java.lang.ref.SoftReference類跟蹤緩存中的對象。這種方法保證這些引用能夠被移除,假如虛擬機的內存用盡而需要更多堆的話。
ClassLoader
Java ClassLoader結構的使用為內存泄漏提供了許多可乘之機。正是該結構本身的復雜性使ClassLoader在內存泄漏方面存在如此多的問題。ClassLoader的非凡之處在于它不僅涉及“常規”的對象引用,還涉及元對象引用,比如:字段、方法和類。這意味著只要有對字段、方法、類或ClassLoader的對象的引用,ClassLoader就會駐留在JVM中。因為ClassLoader本身可以關聯許多類及其靜態字段,所以就有許多內存被泄漏了。
新聞熱點
疑難解答