原文地址:http://dev2dev.bea.com/pub/a/2005/06/memory_leaks.html
消除記憶體流失- -
作者:Staffan Larsen
摘要
雖然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可以顯示堆使用量的圖示。藉助於該圖,可以很容易地看出堆使用量是否隨時間增加。
圖1. 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中的特性使您可以以一種安全而強大的方式跟蹤記憶體流失並進行修複。
參考資料
JRockit工具下載
BEA JRockit 5.0說明文檔
JRockit 5.0中的新功能和新工具
BEA JRockit DevCenter
原文出處
http://dev2dev.bea.com/pub/a/2005/06/memory_leaks.html