處理 Java 程式中的記憶體漏洞

來源:互聯網
上載者:User

研究何時應該關注記憶體漏洞以及如何預防記憶體漏洞 
(作者:IBM DeveloperWorks Jim Patrick) 

  Java 程式中也有記憶體漏洞?當然有。與流行的觀念相反,在 Java 編程中,記憶體管理仍然是需要考慮的問題。在本文中,您將瞭解到什麼會導致記憶體漏洞以及何時應該關注這些漏洞。您還有機會實踐一下在您自己的項目中解決漏洞問題。 

Java 程式中的記憶體漏洞是如何顯現出來的 
  大多數程式員都知道,使用像 Java 這樣的程式設計語言的一大好處就是,他們不必再擔心記憶體的分配和釋放問題。您只須建立對象,當應用程式不再需要這些對象時,Java 會通過一種稱為“垃圾收集”的機制將這些對象刪除。這種處理意味著 Java 已經解決了困擾其他程式設計語言的煩人問題 -- 可怕的記憶體漏洞。真的是這樣的嗎? 

  在深入討論之前,我們先回顧一下垃圾收集的工作方式。垃圾收集器的工作是發現應用程式不再需要的對象,並在這些對象不再被訪問或引用時將它們刪除。垃圾收集器從根節點(在 Java 應用程式的整個生存周期內始終存在的那些類)開始,遍曆被引用的所有節點進行清除。在它遍曆這些節點的同時,它跟蹤哪些對象當前正被引用著。任何類只要不再被引用,它就符合垃圾收集的條件。當刪除這些對象以後,就可將它們所佔用的記憶體資源返回給 JAVA 虛擬機器 (JVM)。 

  所以的確是這樣,Java 代碼不要求程式員負責記憶體的管理和清除,它會自動對無用的對象執行垃圾收集。但是,我們要緊記的一點是僅當一個對象不再被引用時才會被統計為無用。圖 1 說明了這個概念。 
http://www.ccidnet.com/tech/guide/2001/04/12//image/01.jpg 
圖 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 顯示了使用中的堆儲存量隨時間的變化。 
http://www.ccidnet.com/tech/guide/2001/04/12//image/02-s.jpg 
  圖 2. Runtime Heap Summary 

  在 Heap Usage Chart 中,藍色部分表明已指派的堆空間大小。在啟動這個 Java 程式並達到穩定點以後,我強制垃圾收集器運行,在圖中的表現就是綠線(這條線表明插入了一個檢查點)左側的藍線的驟降。隨後,我添加了四個表單,然後又將它們刪除,並再次調用了垃圾收集器。當程式返回僅有一個可視表單的初始狀態時,檢查點之後的藍色地區高於檢查點之前的藍色地區這一情況表明可能存在記憶體漏洞。我通過查看 Instance Summary 證實確實有一個漏洞,因為 Instance Summary 表明 FormFrame 類(它是表單的主使用者介面類)的計數在檢查點之後增加了 4。 

尋找原因 
  為了將測試人員報告的問題剔出,我採取的第一個步驟是找出幾個簡單的、可重複的測試案例。就本例而言,我發現只須添加一個表單,將它刪除,然後強制執行垃圾收集,結果就會導致與被刪除表單相關聯的許多類執行個體仍然處於活動狀態。這個問題在 JProbe 的 Instance Summary 視圖中很明顯,這個視圖統計每個 Java 類在堆中的執行個體數。 

  為了查明使垃圾收集器無法正常完成其工作的那些引用,我使用 JProbe 的 Reference Graph( 3 所示)來確定哪些類仍然引用著目前未被刪除的 FormFrame 類。在調試這個問題時該過程是最複雜的過程之一,因為我發現許多不同的對象仍然引用著這個無用的對象。用來查明究竟是哪個引用者真正造成這個問題的試錯過程相當耗時。 

  在本例中,一個根類(左上方用紅色標明的那個類)是問題的發源地。右側用藍色反白的類處在從最初的 FormFrame 類跟蹤而來的路徑上。 
http://www.ccidnet.com//tech/guide/2001/04/12//image/03-s.jpg 

  圖 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。 

小結 
  尋找記憶體漏洞的原因可能是一個乏味的過程,更不用說需要專用調試工具的情況了。但是,一旦您熟悉了這些工具以及在跟蹤對象引用時進行搜尋的模式,您就能夠找到記憶體漏洞。此外,您還會摸索出一些有價值的技巧,這些技巧不僅有助於節約項目的成本,而且使您能夠領悟到在以後的項目中應該避免哪些編碼方式來預防記憶體漏洞。 

參考資源 
  在開始尋找記憶體漏洞之前,請先熟悉下列一種調試器: 

●Sitraka Software 的 JProbe Profiler withMemory Debugger 
●Intuitive System 的 Optimizeit Java Performance Profiler 
●Paul Moeller 的 Win32Java Heap Inspector 
●IBM alphaWorks 網站上的 Jinsight 
Jinsight: A tool for visualizing the execution of Java programs(developerWorks,1999 年 11 月)詳細說明了這個公用程式是如何協助您分析效能和調試代碼的。 
註:本文討論的項目是用 JDK 1.1.8 完成的,但 JDK 1.2 引入了一個新包,java.lang.ref,這個包可與垃圾收集器互動。另外,JDK 1.2 還引入了一個 java.util.WeakHashMap 類,可用它來代替傳統的 java.util.Hashtable 類。這個類不會阻止垃圾收集器回收鍵對象。JDK 1.3 的 Solaris、Linux 和 Microsoft Windows 版本引入了 Java HotSpot Client VM,該虛擬機器帶有一個新的、經過改進的垃圾收集器。 
作者簡介 
Jim Patrick 是 IBM Pervasive Computing Division 的一名顧問程式員。他從 1996 年開始用 Java 編寫程式。請通過 patrickj@us.ibm.com 與 Jim 聯絡。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.