摘要:從與C/C++內存泄漏對比的角度分析了Java內存泄漏問題,詳細介紹了Java內存泄漏的相關研究和工具,探討了當前研究和工具中存在的不足并分析了其原因,總結了內存泄漏相關領域研究的發展趨勢。
關鍵詞:內存泄漏;垃圾回收;內存低效;Java
中圖法分類號:TP311文獻標識碼:A
文章編號:1001-3695(2006)09-0001-03
1 Java內存泄漏的基本概念
程序執行過程中,由于很多變量所需的內存空間在編譯時無法確定,操作系統需要為這些變量動態地分配內存。相應地,許多編程語言都有其內存分配/回收機制,變量被創建時分配內存,變量不再需要時釋放其所占內存。本文在分析及簡要對比C/C++與Java內存機制的基礎上闡述了Java內存泄漏問題。
C/C++程序運行時,New/Malloc等分配操作將內存塊取到工作空間;Delete/Free等釋放操作將內存塊送回自由空間。程序員通過精心設計分配和釋放操作保證已分配的內存塊或者仍在工作空間,或者被送回自由空間。但是,動態空間中已分配、未被釋放、丟失了訪問路徑的內存塊,由于無法再被訪問而泄漏。C/C++內存泄漏是指程序中已動態分配的堆內存由于某種原因未釋放或無法釋放,造成系統內存的浪費[1]。
Java內存機制與C/C++的一個主要不同是Java內存回收由JVM提供的垃圾收集器(Garbage Collector,GC)自動完成。Java程序員只需通過內存分配操作創建對象,而無須關心對象占用的內存何時被回收。GC監控對象的運行狀態,其中一種是采用可達測試(Reachability Test)[2]檢測對象是否從根(根Root是指Java堆的初始入口點。根對象有五種類型:類的靜態成員、Java方法、本地棧方法、本地方法中的全局變量、線程的異常處理程序中的異常)可達。GC回收對失去引用、從根不可達的對象但仍被引用(直接或間接被根引用)的對象不一定仍有用;對已無用對象的引用導致Java內存泄漏。Java內存泄漏是指邏輯上已無用的對象,由于被其他仍有用的對象引用,導致其無法被回收。常見的泄漏如廢棄的聚集引用(Obsolete Collection References):對象加入到一個聚集中,當其不再需要時并沒有被移去,當該聚集用Static聲明且在程序的整個生命周期存在時問題尤為突出。
從上述介紹可看到,C/C++與Java的內存泄漏問題并不相同,其不同表現形式[3]如圖1所示。
圖1表示程序在某時刻運行的內存狀況,其中節點代表對象,邊表示對象間的引用關系。有用對象是指從該運行時刻開始,程序的路徑將會覆蓋的對象,它對程序行為依然有影響;可達對象是指從該運行時刻開始,根對象可引用到的對象。
可以看到:①C/C++泄漏對象是指已分配,但不可達的對象;Java泄露對象是不再有用,卻仍可達的對象。②C/C++中對象不可達導致的泄漏在Java中被GC回收,因此,與C/C++相比,Java內存泄漏的范圍小,也更難檢測。③
C/C++內存泄漏關注指針及程序變量使用的內存;而對Java程序而言,某些不再有用的對象被其他仍有用的對象引用導致泄漏。所以,Java內存泄漏檢測關注對象間的引用關系及對象是否依然有用。
2 Java內存泄漏現狀
在分析JVM垃圾回收機制的基礎上介紹Java內存泄漏的研究現狀和工具。
2.1JVM垃圾回收機制
Sun’s Java 2 SDK 采用標記及清除(Mark and Sweep)算法(其他常用的GC算法可參見文獻[4])進行可達檢測并完成垃圾回收。其內存回收過程分為兩個階段:①在標記階段,堆中所有對象首先被標記為“False”,可達檢測從根出發依照引用關系追蹤所有對象,若對象可達則將其標記改為“True”;②在回收階段,標記依然為“False”的對象可被回收,如果該對象沒有Finalize方法,其所用內存立刻被回收,否則該對象的 Fina-lize方法首先被執行(Java類的Finalize方法有些類似于C++類的析構函數,但Finalize方法的執行有不確定性,可能導致對象復活,并不能達到與C++析構函數同樣的效果,所以并不推薦使用)[5]。
Finalize方法被執行時,該對象被GC放入終結隊列(Fina-lize Queue),并保留在該隊列中直至JVM啟動一個終結線程(Finalizer Thread)。終結線程將對象從隊列中移出,并執行其Finalize方法。接著,GC進行第二次可達檢測,以檢測Finalize方法是否新創建了對象引用。如果對象沒有通過第二次可達檢測(其標記依然為“False”),GC釋放該對象占用的內存。
產生內存泄漏的原因是不再有用的對象被其他依然有用的對象引用,導致其可以通過可達測試,從而無法被回收。所以,很多針對Java內存泄漏的研究通過分析對象是否有用及對象間的引用關系進行。下面介紹Java內存泄漏研究現狀。
第9期賈曉霞等:Java程序內存泄漏綜述
2.2Java內存泄漏研究現狀
當前對Java內存泄漏檢測的研究非常多,本文主要從以下幾方面介紹:
(1)針對對象是否依然有用
對Java程序而言,很多泄漏的對象都是在一些自包含(Selfcontained)的操作中臨時創建的。操作結束后,這些對象由于被其他依然有用的對象引用,導致無法被GC回收[6]。為了檢測這類內存泄漏,文獻[6]提出了一個基本的內存泄漏場景:對一些臨時對象,給出其期望的生命周期,當對象的存活期限超出其期望的生命周期時,該對象便是一個可疑的泄漏對象。
文獻[7]針對Java中的數組,提出一個結合前向數據流分析和后向控制流分析的算法標志變量間的關系,并確定數組中元素的有用區域:算法給出各程序點相關的變量間的約束關系以及在該點依然有用的數組元素,GC利用數組間的約束關系回收不再有用的數組變量。
(2)針對對象間的引用關系
文獻[8]提出一種基于堆中對象的引用圖,檢測Java應用程序內存泄漏的輕量級自適應方法。由于堆快照中數據量巨大,分析堆中的結構(Data Structure,如一個DOM對象),若該結構在堆中迅速增長,則其可能被泄漏。對可能泄漏的結構定義了一系列度量信息,給出其可能泄漏的優先級,選定某個結構作為候選,尋找其中盡可能以一致方式演化的區域。設計了一個輕量級的方法在程序實際運行時跟蹤這些區域(Region),從而分析可疑的內存泄漏。基于堆快照,裁減對象引用圖,分層次分析得出可疑區域,并在程序實際執行時采用較少的代價確認分析得到的可疑區域是否為內存泄漏。
(3)分析根對象提高內存檢測效率
GC算法從根對象出發進行可達檢測,若某對象從根可達,則保留之。因此,對GC而言,根對象的選擇很重要,如果根對象選擇較保守,則會有很多本應被回收的內存被保留。文獻[9]關注可能成為根對象的局部變量,進行類型分析(Type Analysis)以獲得類型精確的根對象,同時分析變量是否有用使仍可用的對象成為根對象。
(4)垃圾回收算法
除了前面提到的標記及清除算法,還有很多其他的GC算法,如按代回收的回收算法將堆劃分為多個子堆,每個子堆為一“代”對象服務。最年幼的那一代進行最頻繁的垃圾回收,年幼對象經歷了幾次垃圾回收仍然存活就成長一代,移動到另一個子堆中去,如JDK 1.4.1的堆就包括年輕代的堆、年長代的堆和永久保留的堆[10]。自適應回收算法則監視堆中的情況,相應地調整某種簡單垃圾回收算法的參數,或快速轉換到另一種算法,或把堆分為子堆,不同子堆使用不同的GC算法。
上述從幾個不同的角度介紹了Java內存泄漏的已有研究,通過對變量是否可用的分析將已不再有用的對象回收;關注對象引用圖得到可疑對象,然后通過跟蹤實際運行分析之;盡可能縮減GC根對象則可以提高GC的效率;垃圾回收算法的研究也旨在提高GC效率。
2.3Java內存泄漏檢測工具
Java內存泄漏檢測工具獲取程序運行時的內存分配/回收信息,關注堆使用狀況并分析這些信息以發現可疑對象。有兩種不同的方法可獲得內存/分配信息[11]:①采用JVMTI(Java Virtual Machine Tools Interface)或其前身JVMPI(Java Virtual Machine Profiling Interface)。②進行字節碼插裝。JVMPI是Java虛擬機與運行中剖析代理間的雙向函數調用接口:一方面虛擬機通過JVMPI通知剖析代理各種事件;另一方面剖析代理通過JVMPI控制和請求更多的信息。字節碼插裝方法則對插裝后的字節碼進行預處理獲取所需信息。目前大多數工具均是基于JVMPI獲取Java程序運行時刻信息(堆信息及對象的分配、引用關系等)。下面簡要介紹其中的三個工具:(1)JProbe Memory Debugger
利用JProbe Memory Debugger[12]檢測內存泄漏時,用戶設定用例(Use Case)的結果假設與真實結果進行對比發現可疑對象,分析快照(Java虛擬機某個時刻堆的剖面)間的差異發現可疑對象。JProbe Memory Debugger的堆快照提供類視圖(Class View)和實例視圖(Instance View),視圖中提供的信息包括類實例個數、實例變化個數、已分配的內存數等。對某個類實例,可觀測其引用樹(Reference Tree)及引用者樹(Referrer Tree),并在源碼存在時獲得其分配位置。當某個實例為可疑的泄漏對象時,用戶可利用內存泄漏醫生(Memory Leak Doctor)尋找導致泄漏的引用,用戶可利用JProbe提供的被移除引用的列表發現內存泄漏的真正原因。
(2)JRockit Memory Leak Detector
JProbe Memory Debugger[13]基于JVMPI轉儲堆信息獲得堆快照,對其進行離線分析以檢測是否存在內存泄漏。運行時通過JVMPI獲取信息并轉儲堆會降低應用程序的運行速度。與JProbe不同,JRockit Memory Leak Detector是一種實時檢測基于BEA JVM運行的Java程序內存泄漏的工具,它內嵌于 BEA JRockit JVM內,在程序運行時即進行交互式的分析。JRockit通過JRockit 管理控制臺(JRockit Management Console)從運行的JVM中獲得信息。JVM進行一次GC操作,JRockit Momory Leak Detector便獲取數據并進行一次趨勢分析(Trend Analysis)。趨勢分析描述堆中最常用的對象類型(或用戶指定的關注類的對象)、對象當前占用的內存數、占用內存的增長趨勢、對象的實例個數等。增長最快的對象是值得懷疑的泄漏對象。對可疑對象,分析其相互引用關系及指向特定實例的引用來發現導致泄漏的可疑引用。與JProbe相比,JRockit缺乏完整的對象引用圖以及對象的分配軌跡(Allocation Traces)。
(3)Purify
用戶可利用Purify[14]在內存穩定使用時獲取堆快照作為今后快照比較的基準,并在執行可能導致問題的代碼后再次拍攝快照。快照包含方法和對象層次的內存使用情況,基于快照差異分析可得到可能包含內存泄漏的方法或可能的泄漏對象。Purify的調用圖(Call Graph)突出顯示泄漏內存最多的方法并顯示每一方法的詳細數據。用戶可查看對象調用的每一方法并按需將對象排序;從對象列表視圖查看對象的內存使用狀況,以決定大量占用內存的對象是否需要被釋放。
從上述分析可看到,JProbe和Purify都是基于轉儲的堆快照進行離線分析,而JRockit集成于JVM內進行交互式的在線分析。JProbe提供內存泄漏醫生,幫助用戶定位可能引發內存泄漏的引用;而Purify則提供方法的內存使用信息,用戶結合該信息及對象的內存使用信息可在方法層次尋找引發內存泄漏的原因。除了上述三種外,還有很多Java 內存泄漏檢測工具和Java Profiler 工具,如JProfiler[15]的最新版本JProfiler 4支持JVMTI;HeapRoots[16]提供命令行的交互接口分析堆快照;Hprof,Heapdumps, FindRoots和Svcdumps[17]檢測運行于Websphere平臺應用程序的內存泄漏;Optimizeit Enterprise Suite[18],IBM JInsight[19],Oracle JDeveloper[20]等都提供對Java堆信息的提取以幫助用戶檢測內存泄漏。也有一些開源的Java Profiler,如Cougaar Memory Profiler,JMemProf,JMP等[21]。
3 當前研究與工具的不足
針對Java內存泄漏產生的兩個必要條件,即對象可達和對象不再有用,很多研究通過為臨時對象設定期望的生存周期以及采用約束圖表示變量之間的關系來判斷該對象是否仍有用;或者分析對象或類間的引用關系,在發現可疑類或對象后通過對其引用關系的分析發現內存泄漏的根源。而大多數Java內存泄漏檢測工具則基于JVMPI獲取Java程序運行時的堆信息,基于堆中各類對象、對象實例及各類對象占用內存的情況分析內存泄漏,并提供引用圖和對象分配蹤跡棧輔助用戶發現內存泄漏的產生原因。但已有研究和工具還存在以下不足:
(1)研究大都集中于如何發現內存泄漏,但如何對已發現內存進行診斷并去除該泄漏依然是個很難的問題;
(2)大多數工具要求用戶對程序有一定的認識或期望,否則難以從工具提供的海量信息中發現泄漏內存的蹤跡;(3)工具可以對發現泄漏內存提供有價值的信息,但無法對如何診斷泄漏提供有價值的信息。
筆者認為,Java內存泄漏檢測研究是基于對象間的引用關系及對象是否依然有用來檢測內存泄漏或提高GC效率的,但這些信息無法為診斷并去除已檢測到的泄漏提供更多的幫助。Java內存泄漏檢測工具大多基于堆快照中類、對象及其所占內存等信息幫助用戶檢測內存泄漏,基于這些信息同樣很難診斷檢測到的泄漏。這些研究和工具沒有描述并分析對象的行為是其中一個原因。
為了解決這個問題,我們關注Java程序的一個場景:同一類的多個對象,使用邏輯的不同導致其行為的不同,行為的不同可能使得某些對象被回收,而某些對象沒有。筆者提出對象生存期行為模型來描述對象從創建開始到被銷毀為止的整個時間段內的行為,其中對象的行為是采用對象所響應的外部事件以及所發出的請求事件來刻畫的。基于泄漏對象和未泄漏對象行為差異的分析可獲得可能導致泄漏的對象行為,也就是對象響應的外部事件或發出的請求事件,這是筆者正在進行的研究,在該基礎上可深入研究對象行為以診斷更多場景的內存泄漏。
4 總結
內存泄漏可能導致系統性能下降而日益成為廣泛研究的問題。除了不再有用的對象被其他依然有用的對象引用導致內存泄漏外,經分析發現,Java程序中可能存在一種長時間空閑對象。這些對象生存時間很長,但其活動卻只集中于某個短的時間段(該對象長時間處于空閑無活動狀態),導致相應的內存長時間無法循環使用。Java程序某些情況下會出現大量生存期很短的臨時對象,當其占用內存較大時可能導致GC頻繁啟動,消耗大量的程序時間。本文將內存泄漏以及上述兩種情況統稱為Java內存的低效使用。Java內存的低效使用一方面可能導致程序在運行中的某個時刻消耗完JVM所能申請的所有內存而使JVM崩潰;另一方面可能導致頻繁的GC活動而使程序的運行效率急劇下降,這在時間和空間兩個角度上均會產生嚴重的系統性能問題。
內存泄漏的檢測已得到廣泛的研究,支持工具也很多,但檢測內存泄漏的研究或工具采用的信息都無法很好地幫助診斷內存泄漏,如何診斷已泄漏內存是個值得研究的問題。對象生存期行為模型對如何診斷泄漏做了有益的探討,也正是筆者正在進行的研究。
如何更好地診斷Java內存泄漏、如何將內存泄漏問題延伸到內存的低效使用以更好地解決Java程序系統性能下降的問題將是內存泄漏相關領域的重要研究方向。
參考文獻:
[1]The Jargon Dictionary[EB/OL]. http://info.astiran.net/jargon/terms/m/memory_leak.html,2004.
[2]QUEST公司.JProbe Memory Debugger Developer’s Guide[R]. 2005.
[3]Ed Lycklama. Does Java Technology Have Memory Leaks?[R].Chief Technology Officer, KL Group Inc.,2005.
[4]Paul R Wilson. Uniprocessor Garbage Collection Techniques[C]. St. Malo:International Workshop on Memory Management,1992.142.
[5]Bruce Eckel. Thinking in Java (2nd edition)[M].候捷.北京:機械工業出版社,2002.
[6]Wim De Pauw,Gary Sevitsky.Visualizing Reference Patterns for Solving Memory Leaks in Java[C].Lisbon:Proceedings of the 13th European Conference on OO Programming,1999.116134.
[7]R Shaham, E K Kolodner, M Sagiv. Automatic Removal of Array Memory Leaks in Java[C]. Berlin:Compiler Construction,the 9th International Conference, volume 1781 of Lecture Notes in Computer Science, 2000.5066.
[8]N Mitchell, G Sevitsky. Leakbot: An Automated and Lightweight Tool for Diagnosing Memory Leaks in Large Java Applications[C]. Darmstadt: European Conference on ObjectOriented Programming (ECOOP), Lecture Notes in Computer Science, SpringerVerlag,2003.351377.
[9]Agesen O, Detlefs D, MOSS J E B. Garbage Collection and Local Variable Typeprecision and Liveness in Java Virtual Machines[C].New York:Programming Language Design and Implementation (PLDI),1998.269279.
[10]Nagendra Nagarajayya, J Steven Mayer. Improving Java Application Performance and Scalability by Reducing Garbage Collection Times and Sizing Memory Using JDK 1.4.1[EB/OL]. http://developers.sun.com/techtopics/mobility/midp/articles/garbagecollection2/#13,200211.
[11]Staffan Larsen. Memory Leaks, Be Gone[EB/OL]. http://dev2dev.bea.com/pub/a/2005/06/memory_leaks.html,200506.
[12]Quest JProbe Memory Debugger[EB/OL].http://www.quest.com,2005.
[13]BEA JRockit Memory Leak Detector[EB/OL].http://www.bea.com,2005.
[14]IBM Purify[EB/OL].http://www306.ibm.com/software/awd-tools/purify/win/,2005.
[15]EJtechnologies’ JProfiler[EB/OL].http://www.ejtechnologies.com/products/jprofiler/overview.html,2005.
[16]HeapRoots[EB/OL].http://www.alphaworks.ibm.com/tech/heaproots,2005.
[17]Steve Eaton,Hany Salem, Frederic Mora.Finding Java Memory Leaks in WebSphere Application Server[EB/OL].http://www.ibm.com/developerworks/websphere/library/techarticles/0403_mora/0403_mora.html,2005.
[18]Optimizeit Enterprise Suite[EB/OL].http://www.borland.com/us/products/optimizeit/,2005.
[19]IBM JInsight[EB/OL].http://www.research.ibm.com/jinsight/,2005.
[20]Oracle JDeveloper[EB/OL].http://www.oracle.com/technology/products/jdev/,2005.
[21]Open Source Profilers for Java[EB/OL].http://www.manageability.org/blog/stuff/opensourceprofilersforjava/view,200510.
作者簡介:
賈曉霞(1976),女,山西原平人,博士研究生,主要研究方向為軟件測試方法、軟件工程、面向對象技術;
吳際(1974),男,安徽六安人,講師,博士,主要研究方向為軟件測試方法、軟件工程、面向對象技術、編譯技術、自然語言處理技術;
金茂忠(1941),男,上海人,教授,博導,主要研究方向為編譯技術、軟件測試方法、軟件工程、面向對象技術;
李郭歡(1982),男,四川成都人,碩士研究生,主要研究方向為密碼學、軟件測試。