Java 程序里的內存泄漏
譯序:Java 的內存泄漏,這不是一個新話題。Jim Patrick 的這篇文章早在 2001 年就寫出來了。但這并不意味著 Java 的內存泄漏是一個過時了的甚至不重要的話題。相反,Java 的內存泄漏應當是每一個關心程序健壯性、高性能的程序員所必須了解的知識</span>。
本文將揭示什么時候需要關注內存泄漏以及如何進行防止。
摘要:Java 程序里也存在內存泄漏?當然。和流行的看法相反,內存管理仍然是 Java 編程時應該考慮的事情。在這篇文章里,你會了解到是什么原因導致了 Java 內存泄漏以及什么時候需要對這些泄漏進行關注。你也將會學到一個快速實用的課程以應對自己項目中的內存泄漏。
Java 程序里的內存泄漏是如何表現的
大多數程序員都知道使用類似于 Java 的編程語言的好處之一就是他們無需再為內存的分配和釋放所擔心了。你只需要簡單地創建對象,當它們不再為程序所需要時 Java 會自行通過一個被稱為垃圾收集的機制將其移除。這個過程意味著 Java 已經解決了困擾其他編程語言的一個棘手的問題 -- 可怕的內存泄漏。果真是這樣的嗎?
在進行深入討論之前,讓我們先回顧一下垃圾收集是如何進行實際工作的。垃圾收集器的工作就是找到程序不再需要的對象并在當它們不再被訪問或引用時將它們移除掉。垃圾收集器從貫穿整個程序生命周期的類這個根節點開始,掃描所有引用到的節點。在遍歷節點時,它跟蹤那些被活躍引用著的對象。那些不再被引用的對象就滿足了垃圾回收的條件。當這些對象被移除時被它們占用的內存資源會交還給 Java 虛擬機(JVM)。
因此 Java 代碼的確不需要程序員負責內存管理的清理工作,它自行對不再使用的對象進行垃圾收集。然而,需要記住的是,垃圾收集的關鍵在于一個對象在不再被引用時才被統計為不再使用。下圖對這一概念進行了說明。
什么時候需要注意內存泄漏?
如果在你的程序執行一段時間之后遇到 java.lang.OutOfMemoryError 的話,內存泄漏無疑是最值得懷疑的。除了這種明顯的情況之外,什么時候需要考慮內存泄漏?完美主義的程序員會回答說所有的內存泄漏都需要進行審查和更改。然而,在跳到這一結論之前還需要考慮其他幾點因素,包括程序的生命周期以及內存泄漏的大小。
考慮一下在一個程序的生命周期里垃圾收集器可能從未執行的情況。無法保證什么時候 JVM 會調用垃圾收集 -- 即使程序顯式調用 System.gc()</span>。通常情況下,垃圾收集器不會自動運行,直到程序需要比目前可用內存還要多的內存。此時,JVM 會首先嘗試調用垃圾收集器以獲取更多可用內存。如果這個嘗試仍舊不能夠釋放出足夠的資源,JVM 將會從操作系統獲取更多內存,直到達到所允許內存的最大值。
舉個例子來說,一個小型的 Java 應用程序,用來顯示一些簡單的配置修改的用戶界面元素,出現了內存泄漏。垃圾收集器可能在程序關閉之前都不會被調用到,因為 JVM 可能總是有足夠的內存來創建程序所需要的所有對象。因此,在這種情況下,即便是一些已死對象在程序運行的時候仍舊占據著內存,但這并不影響實際應用。
如果開發中的 Java 代碼將以每天 24 小時運行在服務器上,這時內存泄漏將會比上面的那個配置工具程序要明顯的多了。即便是代碼中最小的內存泄漏,在持續運行的情況下最終也將耗盡所有可用內存。
相反的情況下,即使一個程序只是短暫存活,卻分配了大量臨時對象(或者少量的占用大量內存的對象),在這些對象不再需要時沒有取消引用,這樣的 Java 代碼也會達到內存限制。
最后一個值得注意的問題是,不必過于擔心(Java 程序所造成的)內存泄漏。Java 內存泄漏不應該被認為是像其他語言中所發生的那樣危險,比如 C++ 的內存丟失將永遠不會返回給操作系統。Java 應用程序中,我們把不再需要的卻占據著內存資源的對象都交給 JVM。所以在理論上來說,一旦 Java 程序和它的 JVM 關閉掉,所有分配的內存都將歸還給操作系統。
如何斷定程序具有內存泄漏
查看一個運行在 Windows NT 平臺上的 Java 程序是否具有內存泄漏,你可以簡單地在程序運行的時候去觀察任務管理器中的內存設置。然而,在觀察一些運行中的 Java 程序之后,你會發現,它們跟本地應用程序相比使用更多內存。我開發過的一些 Java 項目會啟用 10 到 20 MB 的系統內存。與這個數字相比,本地的操作系統自帶的 Windows Explorer 程序使用到 5 MB。
另外一個關于 Java 程序的內存使用要注意的是典型的運行在 IBM JDK1.1.8 JVM 上的程序似乎在其運行時不斷吞噬了越來越多的系統內存。程序似乎永遠不會返回一些內存給操作系統,直到一個非常大的物理內存分配給它。這會不會就是內存泄漏的跡象?
要明白是怎么回事,我們需要熟悉 JVM 是如何將系統內存使用作自己的堆的。在運行 java.exe 時,你可以使用一些特定的選項來控制垃圾收集的堆的啟動容量和最大容量(分別是 -ms 和 -mx)。Sun 的 JDK 1.1.8 默認使用 1 MB 的啟動設置和 16 MB 的最大設置。IBM JDK 1.1.8 默認使用機器物理內存容量的一半作為最大設置。這些內存設置對 JVM 發生內存溢出時的做法具有直接影響,這時 JVM 可能會繼續增長堆內存,而不是等待一個垃圾回收的結束。
因此為了尋找并最終消除內存泄漏,我們需要比任務監視程序更好的工具。當你想檢測內存泄漏的時候內存調試程序(參見下文的參考資料)可以派上用場了。這些程序通常會給你關于堆內存里對象的數量、每個對象實例的個數以及對象使用中的內存等一些信息。此外,它們還會提供很有用的視圖,這些視圖可以顯示每個對象的引用和引用者,以便你跟蹤內存漏洞的來源。
接下來,我將展示如何使用 Sitraka Software 的 JProbe 調試工具來檢測和消除內存泄漏,希望會對你就如何部署這些工具并成功消除內存泄漏產生一些啟發。
一個內存泄漏的例子
這個示例主要展示了我們部門開發的一個商業版應用的一個問題,這個問題在 JDK 1.1.8 上工作了幾個小時后被測試人員找出來。這個 Java 應用程序的相關代碼和包是由幾個不同團隊的程序員開發出來的。程序里出現的內存泄漏的原因,我懷疑,是由一些沒有真正理解其他(團隊)開發的代碼的程序員所引起。討論中的 Java 代碼允許用戶不必去寫 Palm OS 本地代碼來創建 Palm 個人數碼助理應用。通過使用圖形界面,用戶可以創建表單,使用控件對它們進行填充,然后連接控件事件來創建 Palm 應用程序。測試人員發現,這個 Java 應用最終發生了內存溢出——表單和控件的創建和刪除延時。開發人員并沒有發現這個問題存在,因為他們的機器(相對 Palm)擁有著更多的物理內存。
為了討論這個問題,我使用了 JProbe 來斷定問題的存在。即使擁有 JProde 提供的強大工具和內存快照,調查仍然是一個繁瑣的、反復的過程,它涉及先確定內存泄漏的原因,然后做出代碼更改并驗證其效果。
JProbe 有幾個選項來控制在一次調試回話期間什么樣的信息會被記錄。經過一些試驗后,我判定獲取所需信息的最有效的方式是關掉性能數據收集,專注于捕獲的堆數據。JProbe 提供了一個叫做運行時堆摘要的視圖來顯示 Java 應用程序在一段時間內使用的堆內存的數量。它同時也提供了一個工具欄按鈕用來在需要時強制 JVM 執行垃圾收集 --在想要看一下一個類的給定實例不再為 Java 應用程序需要時是否會被垃圾收集,這個功能是很有用的。下圖顯示了在一段時間內使用的堆存儲量。
尋找原因
要想將測試人員提交的問題隔離出來,第一步就是提供一些簡單的、重復的測試用例。以上面那個例子為例,我發現簡單地添加一個表單,刪除這個表單,然后強制垃圾收集器的結果是一些關聯到已經刪除掉的表單的實例仍然存活著。這種問題通過 JProbe實例摘要視圖來看是顯而易見的,視圖中統計了堆內存中每個類的實例的個數。
要定位垃圾收集器工作時具體實例的引用,我使用了 JProbe 的引用畫面</span>,如下圖所示,來斷定哪些類仍然在引用已被刪除掉的 FormFrame 類。這是調試這種問題的巧妙地方法之一,我通過它發現了很多不同的對象仍然在引用那些無用的對象</span>。而通過試錯來查明究竟是哪個引用者真正造成這個問題的過程卻是相當耗時的。
在這個案例中,根類(左上角紅色的那個)是出現問題的起源。右側用藍色突出的那個類就是追蹤到的 FormFrame 類。
這個字體管理器的問題是,在創建表單時,當代碼將字體向量放進哈希表時,卻沒有定義表單刪除時對向量的移除。因此,這個在整個應用程序的生命周期都存在的靜態的哈希表,卻從來沒有移除指向每個表單的鍵值。所以,所有的表單和其相關聯的類被遺留在了內存中。
問題修正
對于這個問題的簡單解決方案就是字體管理器增加一個方法,來允許哈希表的 remove() 方法會在用戶刪除表單時被調用到。增加的 removeKeyFromHashtables() 方法如下所示:
public void removeKeyFromHashtables(GraphCanvas graph) { if (graph != null) { viewFontTable.remove(graph); // remove key from hashtable // to prevent memory leak } }
然后,我在 FormFrame 類里添加了對這個方法的一個調用。FormFrame 使用 Swing 的內部框架來實現表單 UI,因此對于字體管理器的調用被添加到當內部框架完全關閉時所執行的方法,如下所示:
/**
- Invoked when a FormFrame is disposed. Clean out references to prevent
- memory leaks.
*/
public void internalFrameClosed(InternalFrameEvent e) {
FontManager.get().removeKeyFromHashtables(canvas);
canvas = null;
setDesktopIcon(null);
}</pre>
在我對代碼做出修改以后,我使用調試工具來確認在相同的測試用例被執行時刪除表單所關聯到的對象的數目。
內存泄漏的防止
可以通過對一些常見問題的注意來防止內存泄漏。容器類,比如哈希表和向量,是找到引起內存泄漏的常見的地方。尤其是當這些類被聲明為靜態的并存活于應用程序的整個生命周期之中時。
另一個常見(導致內存泄漏的)問題是當你將一個類注冊為事件監聽器,卻沒考慮到當這個類不再需要時將其注銷。還有,指向其他類的成員變量在恰當的時候要設置為 null。
結束語
尋找內存泄漏的原因可能是一個繁瑣的過程,還沒有提到的一點是這將需要特殊的調試工具。然而,一旦你熟悉了追蹤對象引用的工具和模式,你將能夠跟蹤內存泄漏。此外,你還會獲得一些有價值的技能,不僅可以節省項目編程投入,而且在以后的項目中你將擁有找出可以防止發生內存泄漏的編程做法的眼光。
相關資料 - Quest Software 的 JProbe
- Borland 的 Optimizeit Enterprise
- Paul Moeller 的 Win32 Java Heap Inspector
原文鏈接:http://www.ibm.com/developerworks/library/j-leaks/index.html。