摘要:越來越多的企業(yè)級應用系統(tǒng)采用Java技術開發(fā),這些系統(tǒng)往往長時間運行,哪怕是很小量的內存泄露也有可能導致系統(tǒng)的崩潰,因此內存泄露的問題不容忽視。文章詳細分析了Java系統(tǒng)產(chǎn)生內存泄露的原因和表現(xiàn)形式,提出了預防方法和解決方法。
關鍵詞:Java;內存泄露;內存管理;垃圾收集(GC)
中圖分類號:TP312文獻標識碼:A文章編號:1009-3044(2009)14-3794-03
Research on Memory Leak of Java
ZHU Qiang, CHENG Xiao-hui
(Electronics and Computer Science Department Guilin University of Technology, Guilin Guangxi, 541004)
Abstract: An increasing number of enterprise-class applications using Java technology development, these systems are often long-running, even a small amount of memory leak could lead to system collapse, so the memory leak problem can not be ignored. This paper detailly analyzes the causes and manifestation forms of memory leak which are produced by Java system, andproposes methods of prevention and solutions.
Key words: Java; memory leak; memory management; gargbage collection(GC)
1 引言
隨著人們對網(wǎng)絡程序的安全性要求越來越高,Java以其高安全性的特點迅速成為現(xiàn)代最流行的高級編程語言之一。尤其是它特有的內存管理機制——垃圾收集器(Gargbage Collector,GC),減輕了程序員的負擔,減少了許多內存泄露的可能性,提高了程序的安全性。然而,這并不是說在Java中不存在內存泄露的問題,只是Java的內存泄露比較隱蔽,為了提高程序的安全性和穩(wěn)定性,Java中的內存泄露是值得我們深刻分析一下的。
2 Java內存泄露的概念詮釋
內存泄露,通常是指分配出去后卻無法回收的內存空間。[1]
2.1 傳統(tǒng)語言中的內存泄露
在傳統(tǒng)語言(如C/C++等)中內存泄露的范圍和發(fā)生的可能性是十分大的,程序員需要自行管理內存,如果程序中為變量或對象申請了內存空間,則在不需要時必須調用相應的函數(shù)進行顯式釋放它們占用的內存空間,即使超出變量或對象的作用域,否則這塊內存將永遠得不到回收直至系統(tǒng)重啟。因此可見,傳統(tǒng)語言中一旦發(fā)生內存泄露,其危害性是不言而喻的。
2.2 Java中的內存泄露
針對傳統(tǒng)語言的不足,Java中一個很大的改進就是引入了垃圾回收器(GC)的機制,它使程序員從傳統(tǒng)語言復雜的內存管理中解放出來,將更多的精力關注于業(yè)務邏輯的開發(fā),程序員只需要用關鍵字new或者用Java的反射機機制為對象開辟一塊內存空間,在對象不再使用時,而不需要進行顯式的釋放,這塊空間會被GC自動回收,這種收支兩條線的內存管理機制有效地解決了傳統(tǒng)語言中的內存泄露問題,極大地提高了編程的效率。盡管如此,GC的引入并不能完全避免Java中的內存泄露。Java中的內存泄露和傳統(tǒng)語言中的內存泄露是十分不同的,它是指對象不再被需要時,但卻仍被程序無意識地、錯誤地保持或引用而導致GC無法回收對象所占用的內存空間。因為在GC看來,它們還是“有用”的,即Java中的內存泄露是主觀的內存泄露,是由于程序員的水平或一時大意而造成的。
可以用圖論來描述Java中的內存泄露。把對象看成是有向圖的頂點,引用關系看成是有向圖的有向邊,有向邊從引用對象指向被引用對象,線程對象作為有圖的起始頂點,如圖1。
static List list=new ArrayList();
public static main(String args[]){
Object o1=new Object();
Object o2=new Object();
list.add(o2);
o2=1;}
通過上圖可知,Java中的內存泄露的對象具有以下兩個特點[2]:首先,這些對象是可達的,即在有向圖中存在通路可以與其相連;其次,這些對象是無用的,即程序以后不會再使用這些對象。
3 內存泄露的表現(xiàn)形式
由于有了GC的幫助,那些“不可達”的對象將不會再被泄露,Java中內存泄露的機率降到了很低。因此,Java中內存泄露往往并不像傳統(tǒng)語言中那樣表現(xiàn)得很明顯,使程序很快出現(xiàn)致命地錯誤,但往往會在系統(tǒng)運行一段時間后就會暴露出來。
3.1 瞬間泄露
瞬間泄露是指由于程序在短時間內保持了大量的無用對象的引用而導致堆內存不存或耗盡。它對應用程序來說是致命的,在軟件開發(fā)過程中一般都能被檢測出來,因為不解決它,程序是無法正常運行的。其最最明顯的表現(xiàn)形式就是操作過程中程序瞬間拋出java.lang.OutOfMemoryError,即內存溢出。我們可以通過擴大堆內存空間的方式解決瞬間泄露的出現(xiàn),但內存的增長畢竟是有限的,而且這種解決方式很有可能把瞬間泄露轉成下面我們將說的另一種更為隱蔽的泄露形式——緩慢泄露。
3.2 緩慢泄露
緩慢泄露是指程序每次只泄露少量對象,短時間內不足以影響程序的正常運行,但運行時間一長,程序必定會因為內存不足出現(xiàn)java.lang.OutOfMemoryError錯誤。它具有隱蔽性、泄露周期長的特點,所以在開發(fā)過程中最容易被忽視,這部分內存泄露也是Java內存泄露中的最主要形式。下面是一段緩慢泄露的程序。
public class EmulateStack{
private Object[] statck;
private int pointer;
public EmulateStack(int initial){
stack=new Object[initial];
pointer=0;}
public Object outStack(){
pointer--;
return stack[pointer];}
public void intoStack(Object o){
stack[pointer]=o;
pointer++;}
public static void main(String args[]){
EmulateStack es=new EmulateStatck(5);
Object o=new Object();
es.intoStack(o);
es.outStack();})
在上面的例子中,EmulateStack類模擬數(shù)據(jù)結構中的棧,使用intoStack和outStack方法進行進棧和出棧,pointer指向棧頂位置,在main方法中,初始化了一個大小為5的棧,然后把一個Object對象入棧,接著又把它出棧,這時Object對象占用的空間就被回收,事實上并非如此,outStack方法只是減少了棧頂指針pointer的值,棧中仍然保持著對Object對象的引用,該程序每執(zhí)行一次,都會泄露一個Object對象。實際上,我們只要修改outStack方法,即可解決內存泄露的問題。修改如下:
public Object outStack(){
pointer--;
Object o=stack[pointer];
stack[pointer]=1;
return o;}
4 內存泄露的原因
4.1 客觀原因
主要是由于GC的機制所決定的,GC和程序員對垃圾的認知角度是不一樣的。在GC看來,凡是不可達的對象都是垃圾,凡是有句柄指向的對象都是正在使用的對象,不應該被回收;而在程序員看來,程序不再需要使用的對象都是垃圾,而實際上,這些“所謂的垃圾”還是被某些正在使用的對象引用著的,程序員認為它應該被回收,而GC卻不會回收它們。另外,GC參數(shù)的設置不當,也會增大內存泄露的可能性。
4.2 主觀原因
主要是由于程序員的編程水平或疏忽大意而錯誤地、無意識地保持著某些無用對象的引用而造成的,這在Java內存泄露中十分常見。
List list=new ArrayList();
for(int i=0;i<50;i++){
Object o=new Object();
list.add(o);
o=1;}
在上面的例子中,程序循環(huán)申請Object對象,然后將對象加入一個List容器中,然后試圖通過o=1將對象所占用的空間釋放掉,其實這是不可行的。因為List容器還持有對Object對象的引用,所以GC不會回收這些Object對象,只有用list=1或list.remove(o)才能釋放這些對象。
5 內存泄露的解決方法
5.1 提早預防內存泄露
5.1.1 GC調優(yōu)
不同的JVM采用了不同的垃圾回收機制和啟動參數(shù),有的GC是定時啟動,有的是當CPU資源空閑時開始收集垃圾,有的是當堆內存不足時才開始收集。因此,優(yōu)化GC配置對預防內存泄露十分重要。GC的算法和參數(shù)對應用程序的影響是十分大的,不適當?shù)睦厥諜C制和參數(shù)可能為程序的內存泄露埋下了隱患。
下面將以最流行的JVM——SUN公司的HotSpot虛擬機為例來說明一下GC如何調優(yōu)。
HotSpot是用“分代”方式來管理堆空間的,它將整個堆空間分成了三塊:永久代(Permanent Generation)、年老代(Old Generation)、年輕代(Young Generation)。年老代保存反射創(chuàng)建的對象,年輕代保存剛剛實例化的對象,當年輕代被填滿時,GC會將一部分仍存活的年代代對象移入年老代。針對Hotspot的GC,以下幾條優(yōu)化的原則[3]。
1) 最好將-Xms和-Xmx設為相同值,讓-Xmn的值等于-Xmx的1/3;
2) 一個GUI程序最好是每10到20秒間運行一次GC,每次在半秒內完成;
3) 增加Heap空間的大小雖然會降低GC的頻率,但也增加了每次GC的時間,并且GC運行時所有的用戶線程被暫停,也就是GC期間,Java應用程序不做任何工作;
4) 盡可能增大Heap空間,除非應用程序遇到了較長的響應時間;
5.1.2 良好的編程習慣
高效優(yōu)質的代碼可以在很大程度上減少內存泄露的可能性。為了避免內存泄露,最主要的原則就是盡早釋放對“無用”對象的引用,即在對象不再需要時,用“對象=1”的方式顯式釋放對象,以便GC能盡早回收它所占用的內存空間。許多程序員在使用臨時變量時,總是讓它在退出作用域后自動釋放所引用的對象,這對于一些邏輯結構簡單的程序可能影響并不大,但對引用關系較為復雜的大型應用,就有可能對臨時變量還持用一些錯誤引用而導致臨時對象不能被釋放。下面給出幾條提高編碼效率的建議。[4]
①盡量少用臨時對象,臨時對象的存活周期非常短,很快就會變成垃圾,它會使GC頻繁啟動,從而降低應用程序的性能。
②盡量不要顯式調用System.gc(),因為此方法只是建議JVM進行垃圾回收,至于什么時候回收還是不確定的,JVM可能會在不該進行回收時而啟動GC,導致應用程序臨時中斷。
③盡量少用finalize方法,它會使GC的收集時間增長。
④對象在使用時再實例化,無用時盡早釋放對象的引用,即對象句柄=1。
⑤盡量避免在類的構造函數(shù)中創(chuàng)建大量對象,防止在調用其自類的構造方法時造成不必要的內存資源占用。
⑥盡量不要顯式申請數(shù)組空間,這樣會造成堆空間浪費。
⑦能用基本類型的就不要用封裝類型,如能用int型的,就不要用Integer類型。
⑧避免過深的類層次結構和過深的方法調用,因為這兩者都是十分耗內存的。
⑨對于字符串的操作,盡量用StringBuffer類的appand方法,不要使用String及+,因為對String的每次操作都會產(chǎn)生新的對象。
⑩盡量少用static變量,因為它屬于全局變量,直到應用程序退出才會被GC回收。
5.2 內存泄露的檢測
1) 代碼走查:它是安排有經(jīng)驗的開發(fā)人員或對整個程序代碼很了解的人員對系統(tǒng)進行仔細排查,找到內存泄露的地方。它對于引用關系不是太復雜的小型系統(tǒng)往往十分有效。
2) 利用專業(yè)工具:市場上檢測Java內存泄露的工具十分多,如JDK6.0的命令行工具JPS,Borland公司的OptimizeIt,Ej-technologies公司的Jprofiler等,它們的工作原理大同小異,都是通過監(jiān)測Java程序運行時所有對象的創(chuàng)建、釋放等動作,將內存管理的所有信息進行統(tǒng)計、分析、可視化,開發(fā)人員將根據(jù)這些信息判斷程序是否有內存泄露的問題。下面簡單介紹一下Jprofiler查找內存泄露的基本思路。[5]
Jprofiler 5.1.3是一個全功能的Java剖析工具,專用于分析J2SE和J2EE應用程序,它直覺式的GUI讓你可以找到效率瓶頸,抓出內存泄露,并解決執(zhí)行緒的問題。Jprofiler的內存視圖就是用來觀察系統(tǒng)運行時堆內存的大小,實際使用的大小和各個類的實例分配個數(shù)。如圖2,各列自左到右分別為類名稱、當前實例個數(shù)、自上次標記點增長或減少的實例個數(shù)、占用內存的大小,最下一行是當前JVM的匯總數(shù)據(jù)。
在現(xiàn)實生產(chǎn)中,可以分別在系統(tǒng)運行2小時為間隔點,點擊“快照”按鈕,記錄消退時的內存狀態(tài),抓取當時的內存快照,找出對象個數(shù)增長比較靠前的類,記錄這些類的當前對象個數(shù),記錄數(shù)據(jù)后,點擊上面的“標記”按鈕,將該點的狀態(tài)作為下一次記錄數(shù)據(jù)的比較點,一個正常的系統(tǒng)其運行時的內存占用量一般是比較穩(wěn)定的,不會隨著時間的增長而增長,同樣,一個類的對象也是有一個上限值的,不會無限制的增長,我們可以通過得到的內存快照,對這些快照進行綜合全面的分析,如果有某類對象的內存占用空間一直都在增長,那么就可以初略認定該類對象可能存在內存泄露,接下來,我們再只對這些可疑對象進行仔細監(jiān)控分析,必定會找到內存泄露的對象和地方。
6 結論
綜上所述,Java的內存泄露主要是由于一些無用對象被錯誤地保持著,導致它們的空間不能被GC回收造成的。因此,它經(jīng)常并不容易被發(fā)現(xiàn),本文旨在幫助大家更容易地找出內存泄露,解決性能瓶頸,提高程序的穩(wěn)定性。
參考文獻:
[1] 陳小玉.Java內存泄漏泄露問題的改進與研究[J].微型電腦應用,2005,21(7).
[2] 關鋒,盧鐵,關威.關于 Java的內存泄漏[J].信息技術,2003,27(6).
[3] Jonathan Knudsen, Patrick Niemeyer. Learning Java, 3rd Edition[M].O' Reilly, 2005.
[4] 于海雯,劉萍等.Java的內存管理與垃圾收集機制分析[J].電腦知識與技術,2006,20.
[5] 朱穎芳.關于Java語言內存泄漏問題的探討[J].電腦知識與技術,2006(32).