1. 程式人生 > >java中記憶體洩漏的理解

java中記憶體洩漏的理解

JAVA記憶體機制及記憶體洩露

 

一、Java記憶體管理機制
 

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

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

    Java中對記憶體物件的訪問,使用的是引用的方式。在Java程式碼中我們維護一個記憶體物件的引用變數,通過這個引用變數的值,我們可以訪問到對應的記憶體地址中的記憶體物件空間。在Java程式中,這個引用變數本身既可以存放堆記憶體中,又可以放在程式碼棧的記憶體中(與基本資料型別相同)。GC執行緒會從程式碼棧中的引用變數開始跟蹤,從而判定哪些記憶體是正在使用的。如果GC執行緒通過這種方式,無法跟蹤到某一塊堆記憶體,那麼GC就認為這塊記憶體將不再使用了(因為程式碼中已經無法訪問這塊記憶體了)。
 

 

Java 記憶體管理機制與記憶體洩露

 

    通過這種有向圖的記憶體管理方式,當一個記憶體物件失去了所有的引用之後,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,導致程式崩潰。

 

 

三、記憶體洩漏的基本原理

        在C++語言程式中,使用new操作符建立的物件,在使用完畢後應該通過delete操作符顯示地釋放,否則,這些物件將佔用堆空間,永遠沒有辦法得到回收,從而引起記憶體空間的洩漏。如下的簡單程式碼就可以引起記憶體的洩漏:

 
  1. void function(){

  2.     Int[] vec = new int[5];

  3. }



        在function()方法執行完畢後,vec陣列已經是不可達物件,在C++語言中,這樣的物件永遠也得不到釋放,稱這種現象為記憶體洩漏。 

        而Java是通過垃圾收集器(Garbage Collection,GC)自動管理記憶體的回收,程式設計師不需要通過呼叫函式來釋放記憶體,但它只能回收無用並且不再被其它物件引用的那些物件所佔用的空間。在下面的程式碼中,迴圈申請Object物件,並將所申請的物件放入一個Vector中,如果僅僅釋放物件本身,但是因為Vector仍然引用該物件,所以這個物件對GC來說是不可回收的。因此,如果物件加入到Vector後,還必須從Vector中刪除,最簡單的方法就是將Vector物件設定為null。 
 

 
  1. Vector v = new Vector(10);

  2. for (int i = 1; i < 100; i++)

  3. {

  4.     Object o = new Object();

  5.     v.add(o);

  6.     o = null;

  7. }//此時,所有的Object物件都沒有被釋放,因為變數v引用這些物件。



        實際上無用,而還被引用的物件,GC就無能為力了(事實上GC認為它還有用),這一點是導致記憶體洩漏最重要的原因。 

        Java的記憶體回收機制可以形象地理解為在堆空間中引入了重力場,已經載入的類的靜態變數和處於活動執行緒的堆疊空間的變數是這個空間的牽引物件。這裡牽引物件是指按照Java語言規範,即便沒有其它物件保持對它的引用也不能夠被回收的物件,即Java記憶體空間中的本原物件。當然類可能被去載入,活動執行緒的堆疊也是不斷變化的,牽引物件的集合也是不斷變化的。對於堆空間中的任何一個物件,如果存在一條或者多條從某個或者某幾個牽引物件到該物件的引用鏈,則就是可達物件,可以形象地理解為從牽引物件伸出的引用鏈將其拉住,避免掉到回收池中;而其它的不可達物件由於不存在牽引物件的拉力,在重力的作用下將掉入回收池。在圖1中,A、B、C、D、E、F六個物件都被牽引物件所直接或者間接地“牽引”,使得它們避免在重力的作用下掉入回收池。如果TR1-A鏈和TR2-D鏈斷開,則A、B、C三個物件由於失去牽引,在重力的作用下掉入回收池(被回收),D物件也是同樣的原因掉入回收池,而F物件仍然存在一個牽引鏈(TR3-E-F),所以不會被回收,如圖2、3所示。 

         Java 記憶體管理機制與記憶體洩露 
        圖1 初始狀態 

         Java 記憶體管理機制與記憶體洩露 
        圖2 TR1-A鏈和TR2-D鏈斷開,A、B、C、D掉入回收池 

         Java 記憶體管理機制與記憶體洩露 
        圖3 A、B、C、D四個物件被回收 

        通過前面的介紹可以看到,由於採用了垃圾回收機制,任何不可達物件都可以由垃圾收集執行緒回收。因此通常說的Java記憶體洩漏其實是指無意識的、非故意的物件引用,或者無意識的物件保持。無意識的物件引用是指程式碼的開發人員本來已經對物件使用完畢,卻因為編碼的錯誤而意外地儲存了對該物件的引用(這個引用的存在並不是編碼人員的主觀意願),從而使得該物件一直無法被垃圾回收器回收掉,這種本來以為可以釋放掉的卻最終未能被釋放的空間可以認為是被“洩漏了”。 

        這裡通過一個例子來演示Java的記憶體洩漏。假設有一個日誌類Logger,其提供一個靜態的log(String msg)方法,任何其它類都可以呼叫Logger.Log(message)來將message的內容記錄到系統的日誌檔案中。Logger類有一個型別為HashMap的靜態變數temp,每次在執行log(message)方法的時候,都首先將message的值丟入temp中(以當前執行緒+當前時間為鍵),在方法退出之前再從temp中將以當前執行緒和當前時間為鍵的條目刪除。注意,這裡當前時間是不斷變化的,所以log方法在退出之前執行刪除條目的操作並不能刪除方法執行之初丟入的條目。這樣,任何一個作為引數傳給log方法的字串最終由於被Logger的靜態變數temp引用,而無法得到回收,這種違背實現者主觀意圖的無意識的物件保持就是我們所說的Java記憶體洩漏。 

 

 

3.1 一般情況下記憶體洩漏的避免
 

 

    在不涉及複雜資料結構的一般情況下,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. String s = new String(content);

  15. return s.contains(str);

  16. }

  17.  
  18. }


 

 

在這段程式碼中,FileSearch類中有一個函式hasString,用來判斷文件中是否含有指定的字串。流程是先將mFile載入到記憶體中,然後進行判斷。但是,這裡的問題是,將content宣告為了例項變數,而不是本地變數。於是,在此函式返回之後,記憶體中仍然存在整個檔案的資料。而很明顯,這些資料我們後續是不再需要的,這就造成了記憶體的無故浪費。

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

3.2 複雜資料結構中的記憶體洩露問題
 

    在實際的專案中,我們經常用到一些較為複雜的資料結構用於快取程式執行過程中需要的資料資訊。有時,由於資料結構過於複雜,或者我們存在一些特殊的需求(例如,在記憶體允許的情況下,儘可能多的快取資訊來提高程式的執行速度等情況),我們很難對資料結構中資料的生命週期作出明確的界定。這個時候,我們可以使用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的時候,記憶體還沒有釋放,仍然可以對其進行訪問