JAVA內存泄漏,走開!
盡管java虛擬機和垃圾回收機制管理著大部分的內存事務,但是在
垃圾回收(GC)的角色
雖然垃圾回收關心著大部分的問題,包括內存管理,使得程序員的任務顯得更加輕松,但是程序員還是可能犯些錯誤導致內存泄漏問題。GC(垃圾回收)通過遞歸對所有從“根”對象(堆棧中的對象,靜態數據成員,JNI句柄等等)繼承下來的引用進行
內存管理可以說是自動的,但是這并沒有讓程序員脫離內存管理問題。比方說,對于內存的分配(還有釋放)總是存在一定的開銷,盡管這些開銷對程序員來說是隱含的。一個程序如果創建了很多對象,那么它就要比完成相同任務而創建了較少對象的程序執行的速度慢(如果其他的條件都相同)。
文章更多想說的,導致內存泄漏主要的原因是,先前申請了內存空間而忘記了釋放。如果程序中存在對無用對象的引用,那么這些對象就會駐留內存,消耗內存,因為無法讓垃圾回收器驗證這些對象是否不再需要。正如我們前面看到的,如果存在對象的引用,這個對象就被定義為“活動的”,同時不會被釋放。要確定對象所占內存將被回收,程序員就要務必確認該對象不再會被使用。典型的做法就是把對象數據成員設為null或者從集合中移除該對象。注意,當局部變量不需要時,不需明顯的設為null,因為一個方法執行完畢時,這些引用會自動被清理。
從更高一個層次看,這就是所有存在內存管的
典型的泄漏
既然我們知道了在java中確實會存在內存
全局集合
在大型
可以有很多不同的
另一個辦法是計算引用的數量。集合負責跟蹤集合中每個元素的引用者數量。這要求引用者通知集合什么時候已經對元素處理完畢。當引用者的數目為零時,就可以移除集合中的相關元素。
高速緩存
高速緩存是一種用來快速查找已經執行過的操作結果的數據結構。因此,如果一個操作執行很慢的話,你可以先把普通輸入的數據放入高速緩存,然后過些時間再調用高速緩存中的數據。
高速緩存多少還有一點動態實現的意思,當數據操作完畢,又被送入高速緩存。一個典型的算法如下所示:
1. 檢查結果是否在高速緩存中,存在則返回結果;
2. 如果結果不在,那么計算結果;
3. 將結果放入高速緩存,以備將來的操作調用。
這個算法的問題(或者說潛在的
為了避免這種潛在的致命錯誤設計,程序就必須確定高速緩存在他所使用的內存中有一個上界。因此,更好的算法是:
1. 檢查結果是否在高速緩存中,存在則返回結果;
2. 如果結果不在,那么計算結果;
3. 如果高速緩存所占空間過大,移除緩存中舊的結果;
4. 將結果放入高速緩存,以備將來的操作調用。
通過不斷的從緩存中移除舊的結果,我們可以假設,將來,最新輸入的數據可能被重用的幾率要遠遠大于舊的結果。這通常是一個不錯的設想。
這個新的算法會確保高速緩存的容量在預先確定的范圍內。精確的范圍是很難計算的,因為緩存中的對象存在引用時將繼續有效。正確的劃分高速緩存的大小是一個復雜的任務,你必須權衡可使用內存大小和數據快速存取之間的矛盾。
另一個解決這個問題的途徑是使用
類裝載器
Java類裝載器創建就存在很多導致內存泄漏的漏洞。由于類裝載器的復雜結構,使得很難得到內存
定位
常常地,程序內存泄漏的最初跡象發生在出錯之后,在你的程序中得到一個OutOfMemoryError。這種典型的情況發生在產品環境中,而在那里,你希望內存泄漏盡可能的少,調試的可能性也達到最小。
一個OutOfMemoryError常常是內存泄漏的一個標志,有可能應用程序的確用了太多的內存;這個時候,你既不能增加JVM的堆的數量,也不能改變你的程序而使得他減少內存使用。但是,在大多數情況下,一個OutOfMemoryError是內存泄漏的標志。一個
詳細輸出
有很多辦法來監聽垃圾回收器的活動。也許運用最廣泛的就是以:-Xverbose:gc選項運行JVM,然后觀察輸出結果一段時間。
[memory] 10.109-10.235: GC 65536K->16788K (65536K), 126.000 ms
箭頭后的值(在這個例子中 16788K)是垃圾回收后堆的使用量。
控制臺
觀察這些無盡的GC詳細統計輸出是一件非常單調乏味的事情。好在有一些工具來代替我們做這些事情。The JRockit Management Console可以用圖形的方式輸出堆的使用量。通過觀察圖像,我們可以很方便的觀察堆的使用量是否伴隨時間增長。
Figure 1. The JRockit Management Console
管理控制臺甚至可以配置成在堆使用量出現問題(或者其他的事件發生)時向你發送郵件。這個顯然使得監控內存泄漏更加容易。
內存泄漏探測工具
有很多專門的內存
專門工具的優勢
一旦你知道程序中存在
Java虛擬機工具接口(JVMTI)和他的原有形式JVMPI(壓型接口,profiling Interface)都是標準接口,作為外部工具同JVM進行通信,搜集JVM的信息。字節碼儀器則是引用通過探針獲得工具所需的字節信息的預處理技術。
通過這些技術來偵測內存泄漏存在兩個缺點,而這使得他們在產品級環境中的運用不夠理想。首先,根據兩者對內存的使用量和內存事務性能的降級是不可以忽略的。從JVM獲得的堆的使用量信息需要在工具中導出,收集和處理。這意味著要分配內存。按照JVM的性能導出信息是需要開銷的,垃圾回收器在搜集信息的時候是運行的非常緩慢的。另一個缺點就是,這些工具所需要的信息是關系到JVM的。讓工具在JVM開始運行的時候和它關聯,而在分析的時候,分離工具而保持 JVM運行,這顯然是不可能的。
既然JRockit Memory Leak Detector是被集成到JVM中的,那么以上兩種缺點就不再存在。首先,大部分的處理和分析都是在JVM中完成的,所以就不再需要傳送或重建任何數據。處理也可以建立在垃圾回收器的
趨勢分析
讓我們更深一步來觀察這個工具,了解他如何捕捉到內存泄漏。在你了解到代碼中存在內存泄漏,第一步就是嘗試計算出什么數據在泄漏——哪個對象類導致泄露。The JRockit Memory Leak Detector通過在垃圾回收的時候,計算每個類所包含的現有的對象來達到目的。如果某一個類的對象成員數目隨著時間增長(增長率),那么這里很可能存在泄漏。
Figure 2. The trend analysis view of the Memory Leak Detector
因為一個
一開始,
尋找根本原因
知道那些對象的類會導致泄露,有時候足夠制止泄露問題。這個類也許只是被用在非常有限的部分,通過快速的視察就可以找到問題所在。不幸的是,這些信息是不夠的。
比方說,經常導致內存泄漏的對象類
我們想知道的是其他的對象是否會導致
Figure 3. Sample view of the type graph as seen in the tool
向后工作
自從開始我們就一直著眼于對象類,而不是單獨的對象,我們不知道那個Hashtable存在泄漏。如果我們可以找出所有的Hashtable在系統中有多大,我們可以假設最大的那個Hashtable存在泄漏(因為它可以聚集足夠的泄漏而變得很大)。因此,所有Hashtable,同時有和所有他們所涉及的數據,可以幫助我們查明導致泄露的精確的Hashtable。
Figure 4. Screenshot of the list of Hashtable objects and the size of the data they are holding live
計算一個對象所涉及的數據的開銷是非常大的(這要求引用
Figure 5. Screenshot of the listing of the largest Hashtable entry arrays, as well as their sizes.
向下深入
當我們發現了存在泄漏的Hashtable的實例,就可以順藤摸瓜找到其他的引用這些Hashtable的實例,然后用上面的方法來找到是那個Hashtable存在問題。
Figure 6. This is what an instance graph can look like in the tool.
舉個例子,一個Hashtable可以有一個來自MyServer的對象的引用,而MyServer包含一個activeSessions數據成員。這些信息就足夠深入代碼找出問題所在。
Figure 7. Inspecting an object and its references to other objects
找出分配點
當發現了內存泄漏問題,找到那些泄漏的對象在何處是非常有用的。也許沒有足夠的信息知道他們同其他相關對象之間的聯系,但是關于他們在那里被創建的信息還是很有幫助的。當然,你不會愿意創建一個工具來打印出所有分配的堆棧路徑。你也不會愿意在模擬環境中運行程序只是為了捕捉到一個內存泄漏。
有了JRockit Memory Leak Detector,程序代碼可以動態的在內存分配出創建堆棧路徑。這些堆棧路徑可以在工具中累積,分析。如果你不啟用這個工具,這個特征就不會有任何消耗,這就意味著時刻準備著開始。當需要分配路徑時,JRockit的編譯器可以讓代碼不工作,而監視內存分配,但只對需要的特定類有效。更好的是,當做完數據分析后,生成的機械代碼會完全被移除,不會引起任何執行上的效率衰退。
Figure 8. The allocation stack traces for String during execution of a sample program
總結
內存泄漏查找起來非常困難,文章中的一些避免泄漏的好的實踐,包括了要時刻記住把什么放進了數據結構中,更接近的監視內存中意外的增長。
我們同時也看到了JRockit Memory Leak Detector是如何捕捉產品級系統中的內存泄漏的。該工具通過三步的方法發現泄漏。一,通過趨勢分析發現那些對象類存在泄漏;二,找出同泄漏對象相關的其他類;三,向下發掘,觀察獨立的對象之間是如何相互聯系的。同時,該工具也可以動態的,找出所有內存分配的堆棧路徑。利用這三個特性,將該工具緊緊地集成在JVM中,那么就可以安全的,有效的捕捉和修復內存泄漏了。
資源
JRockit Tools Download
BEA JRockit 5.0 Documentation
New Features and Tools in JRockit 5.0
BEA JRockit DevCenter
Staffan Larsen是JRockit項目的工程師之一,這個項目是在1998年底他與別人聯合創建的。