Java 程序裡的內存洩漏是如何表現的
大多數程序員都知道使用類似於 Java 的編程語言的好處之一就是他們無需再為內存的分配和釋放所擔心了你只需要簡單地創建對象當它們不再為程序所需要時 Java 會自行通過一個被稱為垃圾收集的機制將其移除這個過程意味著 Java 已經解決了困擾其他編程語言的一個棘手的問題 可怕的內存洩漏果真是這樣的嗎?
在進行深入討論之前讓我們先回顧一下垃圾收集是如何進行實際工作的垃圾收集器的工作就是找到程序不再需要的對象並在當它們不再被訪問或引用時將它們移除掉垃圾收集器從貫穿整個程序生命周期的類這個根節點開始掃描所有引用到的節點在遍歷節點時它跟蹤那些被活躍引用著的對象那些不再被引用的對象就滿足了垃圾回收的條件當這些對象被移除時被它們占用的內存資源會交還給 Java 虛擬機(JVM)
因此 Java 代碼的確不需要程序員負責內存管理的清理工作它自行對不再使用的對象進行垃圾收集然而需要記住的是垃圾收集的關鍵在於一個對象在不再被引用時才被統計為不再使用下圖對這一概念進行了說明
上圖表示在一個 Java 程序執行時具有不同的生命周期的兩個類類 A 首先被實例化它存在的時間比較長幾乎貫穿整個進程的生命周期在某個時間點類 B 被創建類 A 添加了一個對這個新建類的引用我們假設類 B 是某個用於顯示並返回用戶指令的用戶界面部件盡管類 B 不再被使用如果類 A 對類 B 的引用未被清除類 B 將繼續存在並占據內存空間即使下一次垃圾收集被執行
什麼時候需要注意內存洩漏?
如果在你的程序執行一段時間之後遇到 javalangOutOfMemoryError 的話內存洩漏無疑是最值得懷疑的除了這種明顯的情況之外什麼時候需要考慮內存洩漏?完美主義的程序員會回答說所有的內存洩漏都需要進行審查和更改然而在跳到這一結論之前還需要考慮其他幾點因素包括程序的生命周期以及內存洩漏的大小
考慮一下在一個程序的生命周期裡垃圾收集器可能從未執行的情況無法保證什麼時候 JVM 會調用垃圾收集 即使程序顯式調用 Systemgc()通常情況下垃圾收集器不會自動運行直到程序需要比目前可用內存還要多的內存此時JVM 會首先嘗試調用垃圾收集器以獲取更多可用內存如果這個嘗試仍舊不能夠釋放出足夠的資源JVM 將會從操作系統獲取更多內存直到達到所允許內存的最大值
舉個例子來說一個小型的 Java 應用程序用來顯示一些簡單的配置修改的用戶界面元素出現了內存洩漏垃圾收集器可能在程序關閉之前都不會被調用到因為 JVM 可能總是有足夠的內存來創建程序所需要的所有對象因此在這種情況下即便是一些已死對象在程序運行的時候仍舊占據著內存但這並不影響實際應用
如果開發中的 Java 代碼將以每天 小時運行在服務器上這時內存洩漏將會比上面的那個配置工具程序要明顯的多了即便是代碼中最小的內存洩漏在持續運行的情況下最終也將耗盡所有可用內存
相反的情況下即使一個程序只是短暫存活卻分配了大量臨時對象(或者少量的占用大量內存的對象)在這些對象不再需要時沒有取消引用這樣的 Java 代碼也會達到內存限制
最後一個值得注意的問題是不必過於擔心(Java 程序所造成的)內存洩漏Java 內存洩漏不應該被認為是像其他語言中所發生的那樣危險比如 C++ 的內存丟失將永遠不會返回給操作系統Java 應用程序中我們把不再需要的卻占據著內存資源的對象都交給 JVM所以在理論上來說一旦 Java 程序和它的 JVM 關閉掉所有分配的內存都將歸還給操作系統
如何斷定程序具有內存洩漏
查看一個運行在 Windows NT 平台上的 Java 程序是否具有內存洩漏你可以簡單地在程序運行的時候去觀察任務管理器中的內存設置然而在觀察一些運行中的 Java 程序之後你會發現它們跟本地應用程序相比使用更多內存我開發過的一些 Java 項目會啟用 到 MB 的系統內存與這個數字相比本地的操作系統自帶的 Windows Explorer 程序使用到 MB
另外一個關於 Java 程序的內存使用要注意的是典型的運行在 IBM JDK JVM 上的程序似乎在其運行時不斷吞噬了越來越多的系統內存程序似乎永遠不會返回一些內存給操作系統直到一個非常大的物理內存分配給它這會不會就是內存洩漏的跡象?
要明白是怎麼回事我們需要熟悉 JVM 是如何將系統內存使用作自己的堆的在運行 javaexe 時你可以使用一些特定的選項來控制垃圾收集的堆的啟動容量和最大容量(分別是 ms 和 mx)Sun 的 JDK 默認使用 MB 的啟動設置和 MB 的最大設置IBM JDK 默認使用機器物理內存容量的一半作為最大設置這些內存設置對 JVM 發生內存溢出時的做法具有直接影響這時 JVM 可能會繼續增長堆內存而不是等待一個垃圾回收的結束
因此為了尋找並最終消除內存洩漏我們需要比任務監視程序更好的工具當你想檢測內存洩漏的時候內存調試程序(參見下文的參考資料)可以派上用場了這些程序通常會給你關於堆內存裡對象的數量每個對象實例的個數以及對象使用中的內存等一些信息此外它們還會提供很有用的視圖這些視圖可以顯示每個對象的引用和引用者以便你跟蹤內存漏洞的來源
接下來我將展示如何使用 Sitraka Software 的 JProbe 調試工具來檢測和消除內存洩漏希望會對你就如何部署這些工具並成功消除內存洩漏產生一些啟發
一個內存洩漏的例子
這個示例主要展示了我們部門開發的一個商業版應用的一個問題這個問題在 JDK 上工作了幾個小時後被測試人員找出來這個 Java 應用程序的相關代碼和包是由幾個不同團隊的程序員開發出來的程序裡出現的內存洩漏的原因我懷疑是由一些沒有真正理解其他(團隊)開發的代碼的程序員所引起討論中的 Java 代碼允許用戶不必去寫 Palm OS 本地代碼來創建 Palm 個人數碼助理應用通過使用圖形界面用戶可以創建表單使用控件對它們進行填充然後連接控件事件來創建 Palm 應用程序測試人員發現這個 Java 應用最終發生了內存溢出表單和控件的創建和刪除延時開發人員並沒有發現這個問題存在因為他們的機器(相對 Palm)擁有著更多的物理內存
為了討論這個問題我使用了 JProbe 來斷定問題的存在即使擁有 JProde 提供的強大工具和內存快照調查仍然是一個繁瑣的反復的過程它涉及先確定內存洩漏的原因然後做出代碼更改並驗證其效果
JProbe 有幾個選項來控制在一次調試回話期間什麼樣的信息會被記錄
經過一些試驗後
我判定獲取所需信息的最有效的方式是關掉性能數據收集
專注於捕獲的堆數據
JProbe 提供了一個叫做運行時堆摘要的視圖來顯示 Java 應用程序在一段時間內使用的堆內存的數量
它同時也提供了一個工具欄按鈕用來在需要時強制 JVM 執行垃圾收集
在想要看一下一個類的給定實例不再為 Java 應用程序需要時是否會被垃圾收集
這個功能是很有用的
下圖顯示了在一段時間內使用的堆存儲量
在堆使用情況圖中藍色部分表示已分配的堆空間量我啟動 Java 程序之後它達到了一個穩定點我強制垃圾收集器執行這由綠線之前的藍色曲線的一個驟降表示(這條綠線表示一個檢查點被插入)接下來我先是添加而後刪掉了四個表單並再次調用垃圾收集器檢查點之後的藍色曲線的水平線比檢查點之前的藍色曲線的水平線高的事實告訴我們很可能出現了內存洩漏因為該程序已經回歸其只有一個簡單可見的表單的初始狀態我檢查實例確認了洩漏總之結果表明 FormFrame 類(表單的主 UI 類)的數量在檢查點之後增加了四個
尋找原因
要想將測試人員提交的問題隔離出來第一步就是提供一些簡單的重復的測試用例以上面那個例子為例我發現簡單地添加一個表單刪除這個表單然後強制垃圾收集器的結果是一些關聯到已經刪除掉的表單的實例仍然存活著這種問題通過 JProbe實例摘要視圖來看是顯而易見的視圖中統計了堆內存中每個類的實例的個數
要定位垃圾收集器工作時具體實例的引用我使用了 JProbe 的引用畫面如下圖所示來斷定哪些類仍然在引用已被刪除掉的 FormFrame 類這是調試這種問題的巧妙地方法之一我通過它發現了很多不同的對象仍然在引用那些無用的對象而通過試錯來查明究竟是哪個引用者真正造成這個問題的過程卻是相當耗時的
在這個案例中根類(左上角紅色的那個)是出現問題的起源右側用藍色突出的那個類就是追蹤到的 FormFrame 類
對於這個具體的例子找到的罪魁禍首是一個包含一個靜態的哈希表的字體管理類通過引用列表追蹤後我發現根節點是一個靜態的哈希表這個哈希表保存了每個表單使用的字體各種表單可以被獨立地放大或縮小所以哈希表包含了一個具有每個指定的表單的所有字體的向量當表單的縮放視圖改變時帶有字體的向量被獲取並選擇合適的縮放因素來適應字體大小
這個字體管理器的問題是在創建表單時當代碼將字體向量放進哈希表時卻沒有定義表單刪除時對向量的移除因此這個在整個應用程序的生命周期都存在的靜態的哈希表卻從來沒有移除指向每個表單的鍵值所以所有的表單和其相關聯的類被遺留在了內存中
問題修正
對於這個問題的簡單解決方案就是字體管理器增加一個方法來允許哈希表的 remove() 方法會在用戶刪除表單時被調用到增加的 removeKeyFromHashtables() 方法如下所示
public void removeKeyFromHashtables(GraphCanvas graph) {
if (graph != null) {
viewFontTableremove(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) {
FontManagerget()removeKeyFromHashtables(canvas)
canvas = null;
setDesktopIcon(null)
}
在我對代碼做出修改以後我使用調試工具來確認在相同的測試用例被執行時刪除表單所關聯到的對象的數目
內存洩漏的防止
可以通過對一些常見問題的注意來防止內存洩漏容器類比如哈希表和向量是找到引起內存洩漏的常見的地方尤其是當這些類被聲明為靜態的並存活於應用程序的整個生命周期之中時
另一個常見(導致內存洩漏的)問題是當你將一個類注冊為事件監聽器卻沒考慮到當這個類不再需要時將其注銷還有指向其他類的成員變量在恰當的時候要設置為 null
結束語
尋找內存洩漏的原因可能是一個繁瑣的過程還沒有提到的一點是這將需要特殊的調試工具然而一旦你熟悉了追蹤對象引用的工具和模式你將能夠跟蹤內存洩漏此外你還會獲得一些有價值的技能不僅可以節省項目編程投入而且在以後的項目中你將擁有找出可以防止發生內存洩漏的編程做法的眼光
From:http://tw.wingwit.com/Article/program/Java/hx/201311/25565.html