您好,登錄后才能下訂單哦!
本篇文章為大家展示了怎樣處理Java程序中的內存漏洞,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
Java 程序中也有內存漏洞?當然有。與流行的觀念相反,在 Java
編程中,內存管理仍然是需要考慮的問題。在本文中,您將了解到什么會導致內存漏洞以及何時應該關注這些漏洞。您還有機會實踐一下在您自己的項目中解決漏洞問題。
Java 程序中的內存漏洞是如何顯現出來的
大多數程序員都知道,使用像 Java
這樣的編程語言的一大好處就是,他們不必再擔心內存的分配和釋放問題。您只須創建對象,當應用程序不再需要這些對象時,Java
會通過一種稱為“垃圾收集”的機制將這些對象刪除。這種處理意味著 Java 已經解決了困擾其他編程語言的煩人問題 -- 可怕的內存漏洞。真的是這樣的嗎?
在深入討論之前,我們先回顧一下垃圾收集的工作方式。垃圾收集器的工作是發現應用程序不再需要的對象,并在這些對象不再被訪問或引用時將它們刪除。垃圾收集器從根節點(在
Java
應用程序的整個生存周期內始終存在的那些類)開始,遍歷被引用的所有節點進行清除。在它遍歷這些節點的同時,它跟蹤哪些對象當前正被引用著。任何類只要不再被引用,它就符合垃圾收集的條件。當刪除這些對象以后,就可將它們所占用的內存資源返回給
Java 虛擬機 (JVM)。
所以的確是這樣,Java
代碼不要求程序員負責內存的管理和清除,它會自動對無用的對象執行垃圾收集。但是,我們要緊記的一點是僅當一個對象不再被引用時才會被統計為無用。圖 1
說明了這個概念。
圖
1. 無用但仍被引用的對象
上面說明了在 Java 應用程序執行期間具有不同生存周期的兩個類。類 A
首先被實例化,并會在很長一段時間或程序的整個生存期內存在。在某個時候,類 B 被創建,類 A 添加對這個新創建的類的一個引用。現在,我們假定類 B
是某個用戶界面小部件,它由用戶顯示甚至解除。如果沒有清除類 A 對 B 的引用,則即便不再需要類 B,并且即便在執行下一個垃圾收集周期以后,類 B
仍將存在并占用內存空間。
何時應該關注內存漏洞
如果您的程序在執行一段時間以后發出 java.lang.OutOfMemoryError
錯誤,則內存漏洞肯定是一個重大嫌疑。除了這種明顯的情況之外,何時還應該關注內存漏洞呢?持完美主義觀點的程序員肯定會回答,應該查找并糾正所有內存漏洞。但是,在得出這個結論之前,還有幾個方面需要考慮,包括程序的生存期和漏洞的大小。
完全有這樣的可能,垃圾收集器在應用程序的生存期內可能始終不會運行。不能保證 JVM 何時以及是否會調用垃圾收集器 -- 即便程序顯式地調用
System.gc() 也是如此。通常,在當前的可用內存能夠滿足程序的內存需求時,JVM 不會自動運行垃圾收集器。當可用內存不能滿足需求時,JVM
將首先嘗試通過調用垃圾收集來釋放出更多的可用內存。如果這種嘗試仍然不能釋放足夠的資源,JVM 將從操作系統獲取更多的內存,直至達到允許的最大極限。
例如,考慮一個小型 Java
應用程序,它顯示一些用于修改配置的簡單用戶界面元素,并且它有一個內存漏洞。很可能到應用程序關閉時也不會調用垃圾收集器,因為 JVM
很可能有足夠的內存來創建程序所需的全部對象,而此后可用內存則所剩無幾。因此,在這種情況下,即使某些“死”對象在程序執行時占用著內存,它實際上并沒有什么用途。
如果正在開發的 Java 代碼要全天 24
小時在服務器上運行,則內存漏洞在此處的影響就比在我們的配置實用程序中的影響要大得多。在要長時間運行的某些代碼中,即使最小的漏洞也會導致 JVM
耗盡全部可用內存。
在相反的情況下,即便程序的生存期較短,如果存在分配大量臨時對象(或者若干吞噬大量內存的對象)的任何 Java
代碼,而且當不再需要這些對象時也沒有取消對它們的引用,則仍然可能達到內存極限。
最后一種情況是內存漏洞無關緊要。我們不應該認為 Java
內存漏洞像其他語言(如 C++)中的漏洞那樣危險,在那些語言中內存將丟失,且永遠不會被返回給操作系統。在 Java
應用程序中,我們使不需要的對象依附于操作系統為 JVM 所提供的內存資源。所以從理論上講,一旦關閉 Java 應用程序及其
JVM,所分配的全部內存將被返回給操作系統。
確定應用程序是否有內存漏洞
為了查看在 Windows NT 平臺上運行的某個 Java
應用程序是否有內存漏洞,您可能試圖在應用程序運行時觀察“任務管理器”中的內存設置。但是,在觀察了運行中的幾個 Java
應用程序以后,您會發現它們比本地應用程序占用的內存要多得多。我做過的一些 Java 項目要使用 10 到 20 MB 的系統內存才能啟動。而操作系統自帶的
Windows Explorer 程序只需 5 MB 左右的內存。
在 Java 應用程序內存使用方面應注意的另一點是,這個典型程序在 IBM
JDK 1.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 的 JProbedebugger
檢測和去除內存漏洞的,以使您對這些工具的部署方式以及成功去除漏洞所需的過程有所了解。
內存漏洞的一個示例
本例集中討論一個問題,我們部門當時正在開發一個商業發行版軟件,這是一個 Java JDK 1.1.8
應用程序,一個測試人員花了幾個小時研究這個程序才最終使這個問題顯現出來。這個 Java
應用程序的基本代碼和包是由幾個不同的開發小組在不同的時間開發的。我猜想,該應用程序中意外出現的內存漏洞是由那些沒有真正理解別人開發的代碼的程序員造成的。
我們正在討論的 Java 代碼允許用戶為 Palm 個人數字助理創建應用程序,而不必編寫任何 Palm OS
本地代碼。通過使用圖形用戶界面,用戶可以創建窗體,向窗體中添加控件,然后連接這些控件的事件來創建 Palm
應用程序。測試人員發現,隨著不斷創建和刪除窗體和控件,這個 Java 應用程序最終會耗盡內存。開發人員沒有檢測到這個問題,因為他們的機器有更多的物理內存。
為了研究這個問題,我用 JProbe 來確定什么地方出了差錯。盡管用了 JProbe
所提供的強大工具和內存快照,研究仍然是一個冗長乏味、不斷重復的過程,首先要確定出現內存漏洞的原因,然后修改代碼,最后還得檢驗結果。
JProbe
提供幾個選項,用來控制調試期間實際記錄哪些信息。經過幾次試驗以后,我斷定獲取所需信息的最有效方法是,關閉性能數據收集,而將注意力集中在所捕獲的堆數據上。JProbe
提供了一個稱為 Runtime Heap Summary 的視圖,它顯示 Java
應用程序運行時所占用的堆內存量隨時間的變化。它還提供了一個工具欄按鈕,必要時可以強制 JVM 執行垃圾收集。如果您試圖弄清楚,當 Java
應用程序不再需要給定的類實例時,這個實例會不會被作為垃圾收集,這個功能將很有用。圖 2 顯示了使用中的堆存儲量隨時間的變化。
圖 2. Runtime Heap Summary
在 Heap Usage Chart 中,藍色部分表明已分配的堆空間大小。在啟動這個 Java
程序并達到穩定狀態以后,我強制垃圾收集器運行,在圖中的表現就是綠線(這條線表明插入了一個檢查點)左側的藍線的驟降。隨后,我添加了四個窗體,然后又將它們刪除,并再次調用了垃圾收集器。當程序返回僅有一個可視窗體的初始狀態時,檢查點之后的藍色區域高于檢查點之前的藍色區域這一情況表明可能存在內存漏洞。我通過查看
Instance Summary 證實確實有一個漏洞,因為 Instance Summary 表明 FormFrame
類(它是窗體的主用戶界面類)的計數在檢查點之后增加了 4。
查找原因
為了將測試人員報告的問題剔出,我采取的第一個步驟是找出幾個簡單的、可重復的測試案例。就本例而言,我發現只須添加一個窗體,將它刪除,然后強制執行垃圾收集,結果就會導致與被刪除窗體相關聯的許多類實例仍然處于活動狀態。這個問題在
JProbe 的 Instance Summary 視圖中很明顯,這個視圖統計每個 Java 類在堆中的實例數。
為了查明使垃圾收集器無法正常完成其工作的那些引用,我使用 JProbe 的 Reference Graph(如圖 3
所示)來確定哪些類仍然引用著目前未被刪除的 FormFrame
類。在調試這個問題時該過程是最復雜的過程之一,因為我發現許多不同的對象仍然引用著這個無用的對象。用來查明究竟是哪個引用者真正造成這個問題的試錯過程相當耗時。
在本例中,一個根類(左上角用紅色標明的那個類)是問題的發源地。右側用藍色突出顯示的類處在從最初的 FormFrame
類跟蹤而來的路徑上。
圖 3. 在引用圖中跟蹤內存漏洞
就本例而言,最后查明罪魁禍首是包含一個靜態 hashtable 的字體管理器類。通過逆向追蹤引用者列表,我發現根節點是用來存儲每個窗體所用字體的一個靜態 hashtable。各個窗體可被單獨放大或縮小,所以這個 hashtable 包含一個具有某個給定窗體的全部字體的 vector。當窗體的大小改變時,就會提取這個字體 vector,并將適當的縮放因子應用于字體大小。
這個字體管理器類的問題是,雖然程序在創建窗體時將字體 vector 存入這個 hashtable 中,但沒有提供在刪除窗體時刪除 vector 的代碼。因此,這個靜態 hashtable(在應用程序的生存期內一直存在)永遠不會刪除引用每個窗體的那些鍵。結果,窗體及其所有關聯的類都閑置在內存中。
修正
本問題的一個簡單解決方案是在字體管理器類中添加一個方法,以便在用戶刪除窗體時以適當的鍵作為參數調用
hashtable 的 remove() 方法。removeKeyFromHashtables() 方法如下所示:
public void removeKeyFromHashtables(GraphCanvas graph) {
if (graph != null) {
viewFontTable.remove(graph); // 刪除 hashtable 中的鍵
// 以預防內存漏洞
}
}
隨后,我在 FormFrame
類中添加了一個對此方法的調用。FormFrame 實際上是使用 Swing
的內部框架來實現窗體用戶界面的,所以我將對字體管理器的調用添加到當完全關閉內部框架時所調用的方法中,如下所示:
/**
* 當去掉 (dispose) FormFrame 時調用。清除引用以預防內存漏洞。
*/
public void internalFrameClosed(InternalFrameEvent e) {
FontManager.get().removeKeyFromHashtables(canvas);
canvas = null;
setDesktopIcon(null);
}
當作了這些修改以后,我使用調試器證實:當執行相同的測試案例時,與被刪除的窗體相關聯的對象計數減小。
預防內存漏洞
可以通過觀察某些常見問題來預防內存漏洞。Collection 類(如 hashtable 和
vector)常常是出現內存漏洞的地方。當這個類被用 static 關鍵字聲明并且在應用程序的整個生存期中存在時尤其是這樣。
另一個常見的問題是,您將一個類注冊為事件監聽程序,而在不再需要這個類時沒有撤銷注冊。此外,您常常需要在適當的時候將指向其他類的類成員變量設置為
null。
小結
查找內存漏洞的原因可能是一個乏味的過程,更不用說需要專用調試工具的情況了。但是,一旦您熟悉了這些工具以及在跟蹤對象引用時進行搜索的模式,您就能夠找到內存漏洞。此外,您還會摸索出一些有價值的技巧,這些技巧不僅有助于節約項目的成本,而且使您能夠領悟到在以后的項目中應該避免哪些編碼方式來預防內存漏洞。
上述內容就是怎樣處理Java程序中的內存漏洞,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。