JAVA內存泄漏--內存泄漏原因和內存泄漏檢測工具(zt)

 摘要
  雖然Java虛擬機(JVM)及其垃圾收集器(garbage collector,GC)負責管理大多數的內存任務,Java軟件程序中還是有可能出現內存泄漏。實際上,這在大型項目中是一個常見的問題。避免內存泄漏的第一步是要弄清楚它是如何發生的。本文介紹了編寫Java代碼的一些常見的內存泄漏陷阱,以及編寫不泄漏代碼的一些最佳實踐。一旦發生了內存泄漏,要指出造成泄漏的代碼是非常困難的。因此本文還介紹了一種新工具,用來診斷泄漏並指出根本原因。該工具的開銷非常小,因此可以使用它來尋找處於生產中的系統的內存泄漏。

垃圾收集器的作用

  雖然垃圾收集器處理了大多數內存管理問題,從而使編程人員的生活變得更輕鬆了,但是編程人員還是可能犯錯而導致出現內存問題。簡單地說,GC循環地跟蹤所有來自“根”對象(堆棧對象、靜態對象、JNI句柄指向的對象,諸如此類)的引用,並將所有它所能到達的對象標記爲活動的。程序只可以操縱這些對象;其他的對象都被刪除了。因爲GC使程序不可能到達已被刪除的對象,這麼做就是安全的。

  雖然內存管理可以說是自動化的,但是這並不能使編程人員免受思考內存管理問題之苦。例如,分配(以及釋放)內存總會有開銷,雖然這種開銷對編程人員來說是不可見的。創建了太多對象的程序將會比完成同樣的功能而創建的對象卻比較少的程序更慢一些(在其他條件相同的情況下)。

  而且,與本文更爲密切相關的是,如果忘記“釋放”先前分配的內存,就可能造成內存泄漏。如果程序保留對永遠不再使用的對象的引用,這些對象將會佔用並耗盡內存,這是因爲自動化的垃圾收集器無法證明這些對象將不再使用。正如我們先前所說的,如果存在一個對對象的引用,對象就被定義爲活動的,因此不能刪除。爲了確保能回收對象佔用的內存,編程人員必須確保該對象不能到達。這通常是通過將對象字段設置爲null或者從集合(collection)中移除對象而完成的。但是,注意,當局部變量不再使用時,沒有必要將其顯式地設置爲null。對這些變量的引用將隨着方法的退出而自動清除。

  概括地說,這就是內存託管語言中的內存泄漏產生的主要原因:保留下來卻永遠不再使用的對象引用。

典型泄漏

  既然我們知道了在Java中確實有可能發生內存泄漏,就讓我們來看一些典型的內存泄漏及其原因。

全局集合

  在大的應用程序中有某種全局的數據儲存庫是很常見的,例如一個JNDI樹或一個會話表。在這些情況下,必須注意管理儲存庫的大小。必須有某種機制從儲存庫中移除不再需要的數據。

  這可能有多種方法,但是最常見的一種是週期性運行的某種清除任務。該任務將驗證儲存庫中的數據,並移除任何不再需要的數據。

  另一種管理儲存庫的方法是使用反向鏈接(referrer)計數。然後集合負責統計集合中每個入口的反向鏈接的數目。這要求反向鏈接告訴集合何時會退出入口。當反向鏈接數目爲零時,該元素就可以從集合中移除了。

緩存

  緩存是一種數據結構,用於快速查找已經執行的操作的結果。因此,如果一個操作執行起來很慢,對於常用的輸入數據,就可以將操作的結果緩存,並在下次調用該操作時使用緩存的數據。

  緩存通常都是以動態方式實現的,其中新的結果是在執行時添加到緩存中的。典型的算法是:

檢查結果是否在緩存中,如果在,就返回結果。
如果結果不在緩存中,就進行計算。
將計算出來的結果添加到緩存中,以便以後對該操作的調用可以使用。
  該算法的問題(或者說是潛在的內存泄漏)出在最後一步。如果調用該操作時有相當多的不同輸入,就將有相當多的結果存儲在緩存中。很明顯這不是正確的方法。

  爲了預防這種具有潛在破壞性的設計,程序必須確保對於緩存所使用的內存容量有一個上限。因此,更好的算法是:

檢查結果是否在緩存中,如果在,就返回結果。
如果結果不在緩存中,就進行計算。
如果緩存所佔的空間過大,就移除緩存最久的結果。
將計算出來的結果添加到緩存中,以便以後對該操作的調用可以使用。
  通過始終移除緩存最久的結果,我們實際上進行了這樣的假設:在將來,比起緩存最久的數據,最近輸入的數據更有可能用到。這通常是一個不錯的假設。

  新算法將確保緩存的容量處於預定義的內存範圍之內。確切的範圍可能很難計算,因爲緩存中的對象在不斷變化,而且它們的引用包羅萬象。爲緩存設置正確的大小是一項非常複雜的任務,需要將所使用的內存容量與檢索數據的速度加以平衡。

  解決這個問題的另一種方法是使用java.lang.ref.SoftReference類跟蹤緩存中的對象。這種方法保證這些引用能夠被移除,如果虛擬機的內存用盡而需要更多堆的話。

ClassLoader

  Java ClassLoader結構的使用爲內存泄漏提供了許多可乘之機。正是該結構本身的複雜性使ClassLoader在內存泄漏方面存在如此多的問題。ClassLoader的特別之處在於它不僅涉及“常規”的對象引用,還涉及元對象引用,比如:字段、方法和類。這意味着只要有對字段、方法、類或ClassLoader的對象的引用,ClassLoader就會駐留在JVM中。因爲ClassLoader本身可以關聯許多類及其靜態字段,所以就有許多內存被泄漏了。

確定泄漏的位置

  通常發生內存泄漏的第一個跡象是:在應用程序中出現了OutOfMemoryError。這通常發生在您最不願意它發生的生產環境中,此時幾乎不能進行調試。有可能是因爲測試環境運行應用程序的方式與生產系統不完全相同,因而導致泄漏只出現在生產中。在這種情況下,需要使用一些開銷較低的工具來監控和查找內存泄漏。還需要能夠無需重啓系統或修改代碼就可以將這些工具連接到正在運行的系統上。可能最重要的是,當進行分析時,需要能夠斷開工具而保持系統不受干擾。

  雖然OutOfMemoryError通常都是內存泄漏的信號,但是也有可能應用程序確實正在使用這麼多的內存;對於後者,或者必須增加JVM可用的堆的數量,或者對應用程序進行某種更改,使它使用較少的內存。但是,在許多情況下,OutOfMemoryError都是內存泄漏的信號。一種查明方法是不間斷地監控GC的活動,確定內存使用量是否隨着時間增加。如果確實如此,就可能發生了內存泄漏。

詳細輸出

  有許多監控垃圾收集器活動的方法。而其中使用最廣泛的可能是使用-Xverbose:gc選項啓動JVM,並觀察輸出。

[memory ] 10.109-10.235: GC 65536K->16788K (65536K), 126.000 ms
 箭頭後面的值(本例中是16788K)是垃圾收集所使用的堆的容量。

控制檯

  查看連續不斷的GC的詳細統計信息的輸出將是非常乏味的。幸好有這方面的工具。JRockit Management Console可以顯示堆使用量的圖示。藉助於該圖,可以很容易地看出堆使用量是否隨時間增加。

Figure 1. The JRockit Management Console

甚至可以配置該管理控制檯,以便如果發生堆使用量過大的情況(或基於其他的事件),控制檯能夠向您發送電子郵件。這明顯使內存泄漏的查看變得更容易了。

內存泄漏檢測工具

  還有其他的專門進行內存泄漏檢測的工具。JRockit Memory Leak Detector可以用來查看內存泄漏,並可以更深入地查出泄漏的根源。這個強大的工具是緊密集成到JRockit JVM中的,其開銷非常小,對虛擬機的堆的訪問也很容易。

專業工具的優點

  一旦知道確實發生了內存泄漏,就需要更專業的工具來查明爲什麼會發生泄漏。JVM自己是不會告訴您的。這些專業工具從JVM獲得內存系統信息的方法基本上有兩種:JVMTI和字節碼技術(byte code instrumentation)。Java虛擬機工具接口(Java Virtual Machine Tools Interface,JVMTI)及其前身Java虛擬機監視程序接口(Java Virtual Machine Profiling Interface,JVMPI)是外部工具與JVM通信並從JVM收集信息的標準化接口。字節碼技術是指使用探測器處理字節碼以獲得工具所需的信息的技術。

  對於內存泄漏檢測來說,這兩種技術有兩個缺點,這使它們不太適合用於生產環境。首先,它們在內存佔用和性能降低方面的開銷不可忽略。有關堆使用量的信息必須以某種方式從JVM導出,並收集到工具中進行處理。這意味着要爲工具分配內存。信息的導出也影響了JVM的性能。例如,當收集信息時,垃圾收集器將運行得比較慢。另外一個缺點是需要始終將工具連在JVM上。這是不可能的:將工具連在一個已經啓動的JVM上,進行分析,斷開工具,並保持JVM運行。

  因爲JRockit Memory Leak Detector是集成到JVM中的,就沒有這兩個缺點了。首先,許多處理和分析工作是在JVM內部進行的,所以沒有必要轉換或重新創建任何數據。處理還可以揹負(piggyback)在垃圾收集器本身上而進行,這意味着提高了速度。其次,只要JVM是使用-Xmanagement選項(允許通過遠程JMX接口監控和管理JVM)啓動的,Memory Leak Detector就可以與運行中的JVM進行連接或斷開。當該工具斷開時,沒有任何東西遺留在JVM中,JVM又將以全速運行代碼,正如工具連接之前一樣。

趨勢分析

  讓我們深入地研究一下該工具以及它是如何用來跟蹤內存泄漏的。在知道發生內存泄漏之後,第一步是要弄清楚泄漏了什麼數據--哪個類的對象引起了泄漏?JRockit Memory Leak Detector是通過在每次垃圾收集時計算每個類的現有對象的數目來實現這一步的。如果特定類的對象數目隨時間而增長(“增長率”),就可能發生了內存泄漏。

 
圖2. Memory Leak Detector的趨勢分析視圖

  因爲泄漏可能像細流一樣非常小,所以趨勢分析必須運行很長一段時間。在短時間內,可能會發生一些類的局部增長,而之後它們又會跌落。但是趨勢分析的開銷很小(最大開銷也不過是在每次垃圾收集時將數據包由JRockit發送到Memory Leak Detector)。開銷不應該成爲任何系統的問題--即使是一個全速運行的生產中的系統。

  起初數目會跳躍不停,但是一段時間之後它們就會穩定下來,並顯示出哪些類的數目在增長。

找出根本原因

  有時候知道是哪些類的對象在泄漏就足以說明問題了。這些類可能只用於代碼中的非常有限的部分,對代碼進行一次快速檢查就可以顯示出問題所在。遺憾地是,很有可能只有這類信息還並不夠。例如,常見到泄漏出在類java.lang.String的對象上,但是因爲字符串在整個程序中都使用,所以這並沒有多大幫助。

  我們想知道的是,另外還有哪些對象與泄漏對象關聯?在本例中是String。爲什麼泄漏的對象還存在?哪些對象保留了對這些對象的引用?但是能列出的所有保留對String的引用的對象將會非常多,以至於沒有什麼實際用處。爲了限制數據的數量,可以將數據按類分組,以便可以看出其他哪些對象的類與泄漏對象(String)關聯。例如,String在Hashtable中是很常見的,因此我們可能會看到與String關聯的Hashtable數據項對象。由Hashtable數據項倒推,我們最終可以找到與這些數據項有關的Hashtable對象以及String(如圖3所示)。

 
圖3. 在工具中看到的類型圖的示例視圖

倒推

  因爲我們仍然是以類的對象而不是單獨的對象來看待對象,所以我們不知道是哪個Hashtable在泄漏。如果我們可以弄清楚系統中所有的Hashtable都有多大,我們就可以假定最大的Hashtable就是正在泄漏的那一個(因爲隨着時間的流逝它會累積泄漏而增長得相當大)。因此,一份有關所有Hashtable對象以及它們引用了多少數據的列表,將會幫助我們指出造成泄漏的確切Hashtabl。

 
圖4. 界面:Hashtable對象以及它們所引用數據的數量的列表

  對對象引用數據數目的計算開銷非常大(需要以該對象作爲根遍歷引用圖),如果必須對許多對象都這麼做,將會花很多時間。如果瞭解一點Hashtable的內部實現原理就可以找到一條捷徑。Hashtable的內部有一個Hashtable數據項的數組。該數組隨着Hashtable中對象數目的增長而增長。因此,爲找出最大的Hashtable,我們只需找出引用Hashtable數據項的最大數組。這樣要快很多。

 
圖5. 界面:最大的Hashtable數據項數組及其大小的清單

更進一步

  當找到發生泄漏的Hashtable實例時,我們可以看到其他哪些實例在引用該Hashtable,並倒推回去看看是哪個Hashtable在泄漏。

 
圖 6. 這就是工具中的實例圖

  例如,該Hashtable可能是由MyServer類型的對象在名爲activeSessions的字段中引用的。這種信息通常就足以查找源代碼以定位問題所在了。

 
圖7. 檢查對象以及它對其他對象的引用

找出分配位置

  當跟蹤內存泄漏問題時,查看對象分配到哪裏是很有用的。只知道它們如何與其他對象相關聯(即哪些對象引用了它們)是不夠的,關於它們在何處創建的信息也很有用。當然了,您並不想創建應用程序的輔助構件,以打印每次分配的堆棧跟蹤(stack trace)。您也不想僅僅爲了跟蹤內存泄漏而在運行應用程序時將一個分析程序連接到生產環境中。

  藉助於JRockit Memory Leak Detector,應用程序中的代碼可以在分配時進行動態添加,以創建堆棧跟蹤。這些堆棧跟蹤可以在工具中進行累積和分析。只要不啓用就不會因該功能而產生成本,這意味着隨時可以進行分配跟蹤。當請求分配跟蹤時,JRockit 編譯器動態插入代碼以監控分配,但是隻針對所請求的特定類。更好的是,在進行數據分析時,添加的代碼全部被移除,代碼中沒有留下任何會引起應用程序性能降低的更改。

 
圖8. 示例程序執行期間String的分配的堆棧跟蹤

結束語

  內存泄漏是難以發現的。本文重點介紹了幾種避免內存泄漏的最佳實踐,包括要始終記住在數據結構中所放置的內容,以及密切監控內存使用量以發現突然的增長。

  我們都已經看到了JRockit Memory Leak Detector是如何用於生產中的系統以跟蹤內存泄漏的。該工具使用一種三步式的方法來找出泄漏。首先,進行趨勢分析,找出是哪個類的對象在泄漏。接下來,看看有哪些其他的類與泄漏的類的對象相關聯。最後,進一步研究單個對象,看看它們是如何互相關聯的。也有可能對系統中所有對象分配進行動態的堆棧跟蹤。這些功能以及該工具緊密集成到JVM中的特性使您可以以一種安全而強大的方式跟蹤內存泄漏並進行修復。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章