Java 程序中的內(nèi)存漏洞是如何顯現(xiàn)出來的
大多數(shù)程序員都知道,使用像 Java 這樣的編程語言的一大好處就是,他們不必再擔心內(nèi)存的分配和釋放問題。您只須創(chuàng)建對象,當應用程序不再需要這些對象時,Java 會通過一種稱為“垃圾收集”的機制將這些對象刪除。這種處理意味著 Java 已經(jīng)解決了困擾其他編程語言的煩人問題 -- 可怕的內(nèi)存漏洞。真的是這樣的嗎?
在深入討論之前,我們先回顧一下垃圾收集的工作方式。垃圾收集器的工作是發(fā)現(xiàn)應用程序不再需要的對象,并在這些對象不再被訪問或引用時將它們刪除。垃圾收集器從根節(jié)點(在 Java 應用程序的整個生存周期內(nèi)始終存在的那些類)開始,遍歷被引用的所有節(jié)點進行清除。在它遍歷這些節(jié)點的同時,它跟蹤哪些對象當前正被引用著。任何類只要不再被引用,它就符合垃圾收集的條件。當刪除這些對象以后,就可將它們所占用的內(nèi)存資源返回給 Java 虛擬機 (JVM)。
所以的確是這樣,Java 代碼不要求程序員負責內(nèi)存的管理和清除,它會自動對無用的對象執(zhí)行垃圾收集。但是,我們要緊記的一點是僅當一個對象不再被引用時才會被統(tǒng)計為無用。圖 1 說明了這個概念。
圖 1. 無用但仍被引用的對象
何時應該關注內(nèi)存漏洞
如果您的程序在執(zhí)行一段時間以后發(fā)出 java.lang.OutOfMemoryError 錯誤,則內(nèi)存漏洞肯定是一個重大嫌疑。除了這種明顯的情況之外,何時還應該關注內(nèi)存漏洞呢?持完美主義觀點的程序員肯定會回答,應該查找并糾正所有內(nèi)存漏洞。但是,在得出這個結論之前,還有幾個方面需要考慮,包括程序的生存期和漏洞的大小。
完全有這樣的可能,垃圾收集器在應用程序的生存期內(nèi)可能始終不會運行。不能保證 JVM 何時以及是否會調(diào)用垃圾收集器 -- 即便程序顯式地調(diào)用 System.gc() 也是如此。通常,在當前的可用內(nèi)存能夠滿足程序的內(nèi)存需求時,JVM 不會自動運行垃圾收集器。當可用內(nèi)存不能滿足需求時,JVM 將首先嘗試通過調(diào)用垃圾收集來釋放出更多的可用內(nèi)存。如果這種嘗試仍然不能釋放足夠的資源,JVM 將從操作系統(tǒng)獲取更多的內(nèi)存,直至達到允許的最大極限。
例如,考慮一個小型 Java 應用程序,它顯示一些用于修改配置的簡單用戶界面元素,并且它有一個內(nèi)存漏洞。很可能到應用程序關閉時也不會調(diào)用垃圾收集器,因為 JVM 很可能有足夠的內(nèi)存來創(chuàng)建程序所需的全部對象,而此后可用內(nèi)存則所剩無幾。因此,在這種情況下,即使某些“死”對象在程序執(zhí)行時占用著內(nèi)存,它實際上并沒有什么用途。
如果正在開發(fā)的 Java 代碼要全天 24 小時在服務器上運行,則內(nèi)存漏洞在此處的影響就比在我們的配置實用程序中的影響要大得多。在要長時間運行的某些代碼中,即使最小的漏洞也會導致 JVM 耗盡全部可用內(nèi)存。
在相反的情況下,即便程序的生存期較短,如果存在分配大量臨時對象(或者若干吞噬大量內(nèi)存的對象)的任何 Java 代碼,而且當不再需要這些對象時也沒有取消對它們的引用,則仍然可能達到內(nèi)存極限。
最后一種情況是內(nèi)存漏洞無關緊要。我們不應該認為 Java 內(nèi)存漏洞像其他語言(如 C++)中的漏洞那樣危險,在那些語言中內(nèi)存將丟失,且永遠不會被返回給操作系統(tǒng)。在 Java 應用程序中,我們使不需要的對象依附于操作系統(tǒng)為 JVM 所提供的內(nèi)存資源。所以從理論上講,一旦關閉 Java 應用程序及其 JVM,所分配的全部內(nèi)存將被返回給操作系統(tǒng)。
確定應用程序是否有內(nèi)存漏洞
為了查看在 Windows NT 平臺上運行的某個 Java 應用程序是否有內(nèi)存漏洞,您可能試圖在應用程序運行時觀察“任務管理器”中的內(nèi)存設置。但是,在觀察了運行中的幾個 Java 應用程序以后,您會發(fā)現(xiàn)它們比本地應用程序占用的內(nèi)存要多得多。我做過的一些 Java 項目要使用 10 到 20 MB 的系統(tǒng)內(nèi)存才能啟動。而操作系統(tǒng)自帶的 Windows Explorer 程序只需 5 MB 左右的內(nèi)存。
在 Java 應用程序內(nèi)存使用方面應注意的另一點是,這個典型程序在 IBM JDK 1.1.8 JVM 中運行時占用的系統(tǒng)內(nèi)存越來越多。似乎直到為它分配非常多的物理內(nèi)存以后它才開始向系統(tǒng)返回內(nèi)存。這些情況是內(nèi)存漏洞的征兆嗎?
要理解其中的緣由,我們必須熟悉 JVM 如何將系統(tǒng)內(nèi)存用作它的堆。當運行 java.exe 時,您使用一定的選項來控制垃圾收集堆的起始大小和最大大小(分別用 -ms 和 -mx 表示)。Sun JDK 1.1.8 的默認起始設置為 1 MB,默認最大設置為 16 MB。IBM JDK 1.1.8 的默認最大設置為系統(tǒng)總物理內(nèi)存大小的一半。這些內(nèi)存設置對 JVM 在用盡內(nèi)存時所執(zhí)行的操作有直接影響。JVM 可能繼續(xù)增大堆,而不等待一個垃圾收集周期的完成。
這樣,為了查找并最終消除內(nèi)存漏洞,我們需要使用比任務監(jiān)視實用程序更好的工具。當您試圖調(diào)試內(nèi)存漏洞時,內(nèi)存調(diào)試程序(請參閱參考資源)可能派得上用場。這些程序通常會顯示堆中的對象數(shù)、每個對象的實例數(shù)和這些對象所占用的內(nèi)存等信息。此外,它們也可能提供有用的視圖,這些視圖可以顯示每個對象的引用和引用者,以便您跟蹤內(nèi)存漏洞的來源。
下面我將說明我是如何用 Sitraka Software 的 JProbedebugger 檢測和去除內(nèi)存漏洞的,以使您對這些工具的部署方式以及成功去除漏洞所需的過程有所了解。
內(nèi)存漏洞的一個示例
本例集中討論一個問題,我們部門當時正在開發(fā)一個商業(yè)發(fā)行版軟件,這是一個 Java JDK 1.1.8 應用程序,一個測試人員花了幾個小時研究這個程序才最終使這個問題顯現(xiàn)出來。這個 Java 應用程序的基本代碼和包是由幾個不同的開發(fā)小組在不同的時間開發(fā)的。我猜想,該應用程序中意外出現(xiàn)的內(nèi)存漏洞是由那些沒有真正理解別人開發(fā)的代碼的程序員造成的。
我們正在討論的 Java 代碼允許用戶為 Palm 個人數(shù)字助理創(chuàng)建應用程序,而不必編寫任何 Palm OS 本地代碼。通過使用圖形用戶界面,用戶可以創(chuàng)建窗體,向窗體中添加控件,然后連接這些控件的事件來創(chuàng)建 Palm 應用程序。測試人員發(fā)現(xiàn),隨著不斷創(chuàng)建和刪除窗體和控件,這個 Java 應用程序最終會耗盡內(nèi)存。開發(fā)人員沒有檢測到這個問題,因為他們的機器有更多的物理內(nèi)存。
為了研究這個問題,我用 JProbe 來確定什么地方出了差錯。盡管用了 JProbe 所提供的強大工具和內(nèi)存快照,研究仍然是一個冗長乏味、不斷重復的過程,首先要確定出現(xiàn)內(nèi)存漏洞的原因,然后修改代碼,最后還得檢驗結果。
JProbe 提供幾個選項,用來控制調(diào)試期間實際記錄哪些信息。經(jīng)過幾次試驗以后,我斷定獲取所需信息的最有效方法是,關閉性能數(shù)據(jù)收集,而將注意力集中在所捕獲的堆數(shù)據(jù)上。JProbe 提供了一個稱為 Runtime Heap Summary 的視圖,它顯示 Java 應用程序運行時所占用的堆內(nèi)存量隨時間的變化。它還提供了一個工具欄按鈕,必要時可以強制 JVM 執(zhí)行垃圾收集。如果您試圖弄清楚,當 Java 應用程序不再需要給定的類實例時,這個實例會不會被作為垃圾收集,這個功能將很有用。圖 2 顯示了使用中的堆存儲量隨時間的變化。
圖 2. Runtime Heap Summary
查找原因
為了將測試人員報告的問題剔出,我采取的第一個步驟是找出幾個簡單的、可重復的測試案例。就本例而言,我發(fā)現(xiàn)只須添加一個窗體,將它刪除,然后強制執(zhí)行垃圾收集,結果就會導致與被刪除窗體相關聯(lián)的許多類實例仍然處于活動狀態(tài)。這個問題在 JProbe 的 Instance Summary 視圖中很明顯,這個視圖統(tǒng)計每個 Java 類在堆中的實例數(shù)。
為了查明使垃圾收集器無法正常完成其工作的那些引用,我使用 JProbe 的 Reference Graph(如圖 3 所示)來確定哪些類仍然引用著目前未被刪除的 FormFrame 類。在調(diào)試這個問題時該過程是最復雜的過程之一,因為我發(fā)現(xiàn)許多不同的對象仍然引用著這個無用的對象。用來查明究竟是哪個引用者真正造成這個問題的試錯過程相當耗時。
在本例中,一個根類(左上角用紅色標明的那個類)是問題的發(fā)源地。右側(cè)用藍色突出顯示的類處在從最初的 FormFrame 類跟蹤而來的路徑上。
圖 3. 在引用圖中跟蹤內(nèi)存漏洞
就本例而言,最后查明罪魁禍首是包含一個靜態(tài) hashtable 的字體管理器類。通過逆向追蹤引用者列表,我發(fā)現(xiàn)根節(jié)點是用來存儲每個窗體所用字體的一個靜態(tài) hashtable。各個窗體可被單獨放大或縮小,所以這個 hashtable 包含一個具有某個給定窗體的全部字體的 vector。當窗體的大小改變時,就會提取這個字體 vector,并將適當?shù)目s放因子應用于字體大小。
這個字體管理器類的問題是,雖然程序在創(chuàng)建窗體時將字體 vector 存入這個 hashtable 中,但沒有提供在刪除窗體時刪除 vector 的代碼。因此,這個靜態(tài) hashtable(在應用程序的生存期內(nèi)一直存在)永遠不會刪除引用每個窗體的那些鍵。結果,窗體及其所有關聯(lián)的類都閑置在內(nèi)存中。
修正
本問題的一個簡單解決方案是在字體管理器類中添加一個方法,以便在用戶刪除窗體時以適當?shù)逆I作為參數(shù)調(diào)用 hashtable 的 remove() 方法。removeKeyFromHashtables() 方法如下所示:
public void removeKeyFromHashtables(GraphCanvas graph) {
if (graph != null) {
viewFontTable.remove(graph); // 刪除 hashtable 中的鍵
// 以預防內(nèi)存漏洞
}
}
隨后,我在 FormFrame 類中添加了一個對此方法的調(diào)用。FormFrame 實際上是使用 Swing 的內(nèi)部框架來實現(xiàn)窗體用戶界面的,所以我將對字體管理器的調(diào)用添加到當完全關閉內(nèi)部框架時所調(diào)用的方法中,如下所示:
/**
* 當去掉 (dispose) FormFrame 時調(diào)用。清除引用以預防內(nèi)存漏洞。
*/
public void internalFrameClosed(InternalFrameEvent e) {
FontManager.get().removeKeyFromHashtables(canvas);
canvas = null;
setDesktopIcon(null);
}
當作了這些修改以后,我使用調(diào)試器證實:當執(zhí)行相同的測試案例時,與被刪除的窗體相關聯(lián)的對象計數(shù)減小。
預防內(nèi)存漏洞
可以通過觀察某些常見問題來預防內(nèi)存漏洞。Collection 類(如 hashtable 和 vector)常常是出現(xiàn)內(nèi)存漏洞的地方。當這個類被用 static 關鍵字聲明并且在應用程序的整個生存期中存在時尤其是這樣。
另一個常見的問題是,您將一個類注冊為事件監(jiān)聽程序,而在不再需要這個類時沒有撤銷注冊。此外,您常常需要在適當?shù)臅r候?qū)⒅赶蚱渌惖念惓蓡T變量設置為 null。
小結
查找內(nèi)存漏洞的原因可能是一個乏味的過程,更不用說需要專用調(diào)試工具的情況了。但是,一旦您熟悉了這些工具以及在跟蹤對象引用時進行搜索的模式,您就能夠找到內(nèi)存漏洞。此外,您還會摸索出一些有價值的技巧,這些技巧不僅有助于節(jié)約項目的成本,而且使您能夠領悟到在以后的項目中應該避免哪些編碼方式來預防內(nèi)存漏洞。
參考資源
在開始查找內(nèi)存漏洞之前,請先熟悉下列一種調(diào)試器:
●Sitraka Software 的 JProbe Profiler withMemory Debugger
●Intuitive System 的 Optimizeit Java Performance Profiler
●Paul Moeller 的 Win32Java Heap Inspector
●IBM alphaWorks 網(wǎng)站上的 Jinsight
Jinsight: A tool for visualizing the execution of Java programs(developerWorks,1999 年 11 月)詳細說明了這個實用程序是如何幫助您分析性能和調(diào)試代碼的。
注:本文討論的項目是用 JDK 1.1.8 完成的,但 JDK 1.2 引入了一個新包,java.lang.ref,這個包可與垃圾收集器交互。另外,JDK 1.2 還引入了一個 java.util.WeakHashMap 類,可用它來代替?zhèn)鹘y(tǒng)的 java.util.Hashtable 類。這個類不會阻止垃圾收集器回收鍵對象。JDK 1.3 的 Solaris、Linux 和 Microsoft Windows 版本引入了 Java HotSpot Client VM,該虛擬機帶有一個新的、經(jīng)過改進的垃圾收集器。
作者簡介
Jim Patrick 是 IBM Pervasive Computing Division 的一名顧問程序員。他從 1996 年開始用 Java 編寫程序。請通過
patrickj@us.ibm.com
與 Jim 聯(lián)系。
更多文章、技術交流、商務合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
