Java內存泄露的理解與解決 – JAVA編程語言程序開發技術文章

轉載請註明出處:blogjava.net/zh-weir/archive/2011/02/23/345007.html


Java內存管理機制



在C++語言中,如果需要動態分配一塊內存,程序員需要負責這塊內存的整個生命周期。從申請分配、到使用、再到最後的釋放。這樣的過程非常靈活,但是卻十分繁瑣,程序員很容易由於疏忽而忘記釋放內存,從而導致內存的泄露。Java語言對內存管理做瞭自己的優化,這就是垃圾回收機制。Java的幾乎所有內存對象都是在堆內存上分配(基本數據類型除外),然後由GC(garbage collection)負責自動回收不再使用的內存。


    上面是Java內存管理機制的基本情況。但是如果僅僅理解到這裡,我們在實際的項目開發中仍然會遇到內存泄漏的問題。也許有人表示懷疑,既然Java的垃圾回收機制能夠自動的回收內存,怎麼還會出現內存泄漏的情況呢?這個問題,我們需要知道GC在什麼時候回收內存對象,什麼樣的內存對象會被GC認為是“不再使用”的。


    Java中對內存對象的訪問,使用的是引用的方式。在Java代碼中我們維護一個內存對象的引用變量,通過這個引用變量的值,我們可以訪問到對應的內存地址中的內存對象空間。在Java程序中,這個引用變量本身既可以存放堆內存中,又可以放在代碼棧的內存中(與基本數據類型相同)。GC線程會從代碼棧中的引用變量開始跟蹤,從而判定哪些內存是正在使用的。如果GC線程通過這種方式,無法跟蹤到某一塊堆內存,那麼GC就認為這塊內存將不再使用瞭(因為代碼中已經無法訪問這塊內存瞭)。



    通過這種有向圖的內存管理方式,當一個內存對象失去瞭所有的引用之後,GC就可以將其回收。反過來說,如果這個對象還存在引用,那麼它將不會被GC回收,哪怕是Java虛擬機拋出OutOfMemoryError。


 


Java內存泄露



    一般來說內存泄漏有兩種情況。一種情況如在C/C++語言中的,在堆中的分配的內存,在沒有將其釋放掉的時候,就將所有能訪問這塊內存的方式都刪掉(如指針重新賦值);另一種情況則是在內存對象明明已經不需要的時候,還仍然保留著這塊內存和它的訪問方式(引用)。第一種情況,在Java中已經由於垃圾回收機制的引入,得到瞭很好的解決。所以,Java中的內存泄漏,主要指的是第二種情況。


    可能光說概念太抽象瞭,大傢可以看一下這樣的例子:


 


1 Vector v=new Vector(10);
2 for (int i=1;i<100; i++){
3 Object o=new Object();
4 v.add(o);
5 o=null;
6 }
  
    在這個例子中,代碼棧中存在Vector對象的引用v和Object對象的引用o。在For循環中,我們不斷的生成新的對象,然後將其添加到Vector對象中,之後將o引用置空。問題是當o引用被置空後,如果發生GC,我們創建的Object對象是否能夠被GC回收呢?答案是否定的。因為,GC在跟蹤代碼棧中的引用時,會發現v引用,而繼續往下跟蹤,就會發現v引用指向的內存空間中又存在指向Object對象的引用。也就是說盡管o引用已經被置空,但是Object對象仍然存在其他的引用,是可以被訪問到的,所以GC無法將其釋放掉。如果在此循環之後,Object對象對程序已經沒有任何作用,那麼我們就認為此Java程序發生瞭內存泄漏。


    盡管對於C/C++中的內存泄露情況來說,Java內存泄露導致的破壞性小,除瞭少數情況會出現程序崩潰的情況外,大多數情況下程序仍然能正常運行。但是,在移動設備對於內存和CPU都有較嚴格的限制的情況下,Java的內存溢出會導致程序效率低下、占用大量不需要的內存等問題。這將導致整個機器性能變差,嚴重的也會引起拋出OutOfMemoryError,導致程序崩潰。


 


一般情況下內存泄漏的避免



    在不涉及復雜數據結構的一般情況下,Java的內存泄露表現為一個內存對象的生命周期超出瞭程序需要它的時間長度。我們有時也將其稱為“對象遊離”。


例如:


 


 1 public class FileSearch{
 2
 3     private byte[] content;
 4     private File mFile;
 5    
 6     public FileSearch(File file){
 7         mFile = file;
 8     }
 9
10     public boolean hasString(String str){
11         int size = getFileSize(mFile);
12         content = new byte[size];
13         loadFile(mFile, content);
14        
15         String s = new String(content);
16         return s.contains(str);
17     }
18 }


    在這段代碼中,FileSearch類中有一個函數hasString,用來判斷文檔中是否含有指定的字符串。流程是先將mFile加載到內存中,然後進行判斷。但是,這裡的問題是,將content聲明為瞭實例變量,而不是本地變量。於是,在此函數返回之後,內存中仍然存在整個文件的數據。而很明顯,這些數據我們後續是不再需要的,這就造成瞭內存的無故浪費。


    要避免這種情況下的內存泄露,要求我們以C/C++的內存管理思維來管理自己分配的內存。第一,是在聲明對象引用之前,明確內存對象的有效作用域。在一個函數內有效的內存對象,應該聲明為local變量,與類實例生命周期相同的要聲明為實例變量……以此類推。第二,在內存對象不再需要時,記得手動將其引用置空。


 


復雜數據結構中的內存泄露問題



    在實際的項目中,我們經常用到一些較為復雜的數據結構用於緩存程序運行過程中需要的數據信息。有時,由於數據結構過於復雜,或者我們存在一些特殊的需求(例如,在內存允許的情況下,盡可能多的緩存信息來提高程序的運行速度等情況),我們很難對數據結構中數據的生命周期作出明確的界定。這個時候,我們可以使用Java中一種特殊的機制來達到防止內存泄露的目的。


    之前我們介紹過,Java的GC機制是建立在跟蹤內存的引用機制上的。而在此之前,我們所使用的引用都隻是定義一個“Object o;”這樣形式的。事實上,這隻是Java引用機制中的一種默認情況,除此之外,還有其他的一些引用方式。通過使用這些特殊的引用機制,配合GC機制,就可以達到一些我們需要的效果。


 


Java中的幾種引用方式



    Java中有幾種不同的引用方式,它們分別是:強引用、軟引用、弱引用和虛引用。下面,我們首先詳細地瞭解下這幾種引用方式的意義。



   
      強引用


在此之前我們介紹的內容中所使用的引用都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當內存空 間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。


 


軟引用(SoftReference)


SoftReference 類的一個典型用途就是用於內存敏感的高速緩存。SoftReference 的原理是:在保持對對象的引用時保證在 JVM 報告內存不足情況之前將清除所有的軟引用。關鍵之處在於,垃圾收集器在運行時可能會(也可能不會)釋放軟可及對象。對象是否被釋放取決於垃圾收集器的算法 以及垃圾收集器運行時可用的內存數量。


 


弱引用(WeakReference)


WeakReference 類的一個典型用途就是規范化映射(canonicalized mapping)。另外,對於那些生存期相對較長而且重新創建的開銷也不高的對象來說,弱引用也比較有用。關鍵之處在於,垃圾收集器運行時如果碰到瞭弱可及對象,將釋放 WeakReference 引用的對象。然而,請註意,垃圾收集器可能要運行多次才能找到並釋放弱可及對象。


 


虛引用(PhantomReference)


PhantomReference 類隻能用於跟蹤對被引用對象即將進行的收集。同樣,它還能用於執行 pre-mortem 清除操作。PhantomReference 必須與 ReferenceQueue 類一起使用。需要 ReferenceQueue 是因為它能夠充當通知機制。當垃圾收集器確定瞭某個對象是虛可及對象時,PhantomReference 對象就被放在它的 ReferenceQueue 上。將 PhantomReference 對象放在 ReferenceQueue 上也就是一個通知,表明 PhantomReference 對象引用的對象已經結束,可供收集瞭。這使您能夠剛好在對象占用的內存被回收之前采取行動。Reference與ReferenceQueue的配合使用。


 


GC、Reference與ReferenceQueue的交互



A、 GC無法刪除存在強引用的對象的內存。


 


B、 GC發現一個隻有軟引用的對象內存,那麼:


① SoftReference對象的referent 域被設置為null,從而使該對象不再引用heap對象。


② SoftReference引用過的heap對象被聲明為finalizable。


③ 當 heap 對象的 finalize() 方法被運行而且該對象占用的內存被釋

發佈留言