Java 記憶體洩露的理解與解決過程

henryyang發表於2015-03-17

本文詳細地介紹了Java記憶體管理的原理,以及記憶體洩露產生的原因,同時提供了一些列解決Java記憶體洩露的方案,希望對各位Java開發者有所幫助。

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 中的記憶體洩漏,主要指的是第二種情況。

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

Vector v = new  Vector( 10 );  
for  ( int  i = 1 ;i < 100 ; i ++ ){  
Object o = new  Object();  
v.add(o);  
o = null ;  
}

在這個例子中,程式碼棧中存在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 的記憶體洩露表現為一個記憶體物件的生命週期超出了程式需要它的時間長度。我們有時也將其稱為“物件遊離”。

例如:

public class FileSearch{  
      private byte [] content;  
      private File mFile;  
     public FileSearch(File file){  
      mFile = file;  
      }  
     public boolean hasString(String str){  
         int size = getFileSize(mFile);  
        content =  new  byte [size];  
         loadFile(mFile, content);  
         String s =  new String(content);  
         return s.contains(str);  
     }  
}

在這段程式碼中,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()  方法被執行而且該物件佔用的記憶體被釋放, SoftReference  物件就被新增到它的  ReferenceQueue (如果後者存在的話)。
C、  GC發現一個只有弱引用的物件記憶體,那麼:
①  WeakReference物件的 referent 域被設定為 null , 從而使該物件不再引用heap 物件。
②  WeakReference引用過的 heap 物件被宣告為 finalizable 。
③  當heap 物件的 finalize() 方法被執行而且該物件佔用的記憶體被釋放時, WeakReference 物件就被新增到它的 ReferenceQueue (如果後者存在的話)。
D、  GC發現一個只有虛引用的物件記憶體,那麼:
①  PhantomReference引用過的 heap 物件被宣告為 finalizable 。
②  PhantomReference在堆物件被釋放之前就被新增到它的 ReferenceQueue 。
值得注意的地方有以下幾點:
1、 GC 在一般情況下不會發現軟引用的記憶體物件,只有在記憶體明顯不足的時候才會發現並釋放軟引用物件的記憶體。
2、 GC 對弱引用的發現和釋放也不是立即的,有時需要重複幾次 GC ,才會發現並釋放弱引用的記憶體物件。
3、軟引用和弱引用在新增到 ReferenceQueue 的時候,其指向真實記憶體的引用已經被置為空了,相關的記憶體也已經被釋放掉了。而虛引用在新增到 ReferenceQueue 的時候,記憶體還沒有釋放,仍然可以對其進行訪問。
程式碼示例

通過以上的介紹,相信您對Java 的引用機制以及幾種引用方式的異同已經有了一定了解。光是概念,可能過於抽象,下面我們通過一個例子來演示如何在程式碼中使用 Reference 機制。

String str  =   new  String( " hello " );  // ①   
ReferenceQueue < String >  rq  =   new  ReferenceQueue < String > ();  // ②   
WeakReference < String >  wf  =   new  WeakReference < String > (str, rq);  // ③   
str = null ;  // ④取消"hello"物件的強引用   
String str1 = wf.get();  // ⑤假如"hello"物件沒有被回收,str1引用"hello"物件  
// 假如"hello"物件沒有被回收,rq.poll()返回null   
Reference <?   extends  String >  ref = rq.poll();  // ⑥

在以上程式碼中,注意⑤⑥兩處地方。假如“hello ”物件沒有被回收 wf.get() 將返回“ hello ”字串物件, rq.poll() 返回 null ;而加入“ hello ”物件已經被回收了,那麼 wf.get() 返回 null , rq.poll() 返回 Reference 物件,但是此 Reference 物件中已經沒有 str 物件的引用了 ( PhantomReference 則與WeakReference 、 SoftReference 不同 )。

引用機制與複雜資料結構的聯合應用

瞭解了GC 機制、引用機制,並配合上 ReferenceQueue ,我們就可以實現一些防止記憶體溢位的複雜資料型別。

例如,SoftReference 具有構建 Cache 系統的特質,因此我們可以結合雜湊表實現一個簡單的快取系統。這樣既能保證能夠儘可能多的快取資訊,又可以保證 Java 虛擬機器不會因為記憶體洩露而丟擲 OutOfMemoryError 。這種快取機制特別適合於記憶體物件生命週期長,且生成記憶體物件的耗時比較長的情況,例如快取列表封面圖片等。對於一些生命週期較長,但是生成記憶體物件開銷不大的情況,使用WeakReference 能夠達到更好的記憶體管理的效果。

附SoftHashmap 的原始碼一份,相信看過之後,大家會對 Reference 機制的應用有更深入的理解。
package  com. *** .widget;  
    // : SoftHashMap.java    
    import  java.util. * ;   
    import  java.lang.ref. * ;   
    import  android.util.Log;  

    public   class  SoftHashMap  extends  AbstractMap  {   
      /**  The internal HashMap that will hold the SoftReference.  */    
      private   final  Map hash  =   new  HashMap();   
      /**  The number of "hard" references to hold internally.  */    
      private   final   int  HARD_SIZE;   
      /**  The FIFO list of hard references, order of last access.  */    
      private   final  LinkedList hardCache  =   new  LinkedList();   
      /**  Reference queue for cleared SoftReference objects.  */    
      private  ReferenceQueue queue  =   new  ReferenceQueue();   
      // Strong Reference number   
      public  SoftHashMap()  {  this ( 100 ); }    
      public  SoftHashMap( int  hardSize)  { HARD_SIZE  =  hardSize; }    

      public  Object get(Object key)  {   
       Object result  =   null ;   
        //  We get the SoftReference represented by that key    
       SoftReference soft_ref  =  (SoftReference)hash.get(key);   
        if  (soft_ref  !=   null )  {   
          //  From the SoftReference we get the value, which can be   
          //  null if it was not in the map, or it was removed in   
          //  the processQueue() method defined below    
        result  =  soft_ref.get();   
          if  (result  ==   null )  {   
            //  If the value has been garbage collected, remove the   
            //  entry from the HashMap.    
           hash.remove(key);   
         }   else   {   
            //  We now add this object to the beginning of the hard   
            //  reference queue.  One reference can occur more than   
            //  once, because lookups of the FIFO queue are slow, so   
            //  we don't want to search through it each time to remove   
            //  duplicates.   
              // keep recent use object in memory   
           hardCache.addFirst(result);   
            if  (hardCache.size()  >  HARD_SIZE)  {   
              //  Remove the last entry if list longer than HARD_SIZE    
             hardCache.removeLast();   
           }    
         }    
       }    
        return  result;   
     }    

      /**  We define our own subclass of SoftReference which contains   
      not only the value but also the key to make it easier to find   
      the entry in the HashMap after it's been garbage collected.  */    
      private   static   class  SoftValue  extends  SoftReference  {   
        private   final  Object key;  //  always make data member final    
        /**  Did you know that an outer class can access private data   
        members and methods of an inner class?  I didn't know that!   
        I thought it was only the inner class who could access the   
        outer class's private information.  An outer class can also   
        access private members of an inner class inside its inner   
        class.  */    
        private  SoftValue(Object k, Object key, ReferenceQueue q)  {   
          super (k, q);   
          this .key  =  key;   
       }    
     }    

      /**  Here we go through the ReferenceQueue and remove garbage   
      collected SoftValue objects from the HashMap by looking them   
      up using the SoftValue.key data member.  */    
      public   void  processQueue()  {   
       SoftValue sv;   
        while  ((sv  =  (SoftValue)queue.poll())  !=   null )  {   
            if (sv.get() ==   null ) {  
               Log.e( " processQueue " ,  " null " );  
           } else {  
               Log.e( " processQueue " ,  " Not null " );  
           }   
         hash.remove(sv.key);  //  we can access private data!   
         Log.e( " SoftHashMap " ,  " release  "   +  sv.key);  
       }    
     }    
      /**  Here we put the key, value pair into the HashMap using   
      a SoftValue object.  */    
      public  Object put(Object key, Object value)  {   
       processQueue();  //  throw out garbage collected values first    
       Log.e( " SoftHashMap " ,  " put into  "   +  key);  
        return  hash.put(key,  new  SoftValue(value, key, queue));   
     }    
      public  Object remove(Object key)  {   
       processQueue();  //  throw out garbage collected values first    
        return  hash.remove(key);   
     }    
      public   void  clear()  {   
       hardCache.clear();   
       processQueue();  //  throw out garbage collected values    
      hash.clear();   
    }    
     public   int  size()  {   
      processQueue();  //  throw out garbage collected values first    
       return  hash.size();   
    }    
     public  Set entrySet()  {   
       //  no, no, you may NOT do that!!! GRRR    
       throw   new  UnsupportedOperationException();   
    }   
  }

相關文章