Java 內存管理機制與內存泄露

jopen 10年前發布 | 52K 次閱讀 Java 內存 Java開發

一、Java內存管理機制

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

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

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

Java 內存管理機制與內存泄露

 

    通過這種有向圖的內存管理方式,當一個內存對象失去了所有的引用之后,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對象的引用vObject對象的引用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操作符顯示地釋放,否則,這些對象將占用堆空間,永遠沒有辦法得到回收,從而引起內存空間的泄漏。如下的簡單代碼就可以引起內存的泄漏:

void function(){
    Int[] vec = new int[5];
}


        在function()方法執行完畢后,vec數組已經是不可達對象,在C++語言中,這樣的對象永遠也得不到釋放,稱這種現象為內存泄漏。

        而Java是通過垃圾收集器(Garbage Collection,GC)自動管理內存的回收,程序員不需要通過調用函數來釋放內存,但它只能回收無用并且不再被其它對象引用的那些對象所占用的空間。在下面的代碼中,循環申請Object對象,并將所申請的對象放入一個Vector中,如果僅僅釋放對象本身,但是因為Vector仍然引用該對象,所以這個對象對GC來說是不可回收的。因此,如果對象加入到Vector后,還必須從Vector中刪除,最簡單的方法就是將Vector對象設置為null。

Vector v = new Vector(10);
for (int i = 1; i < 100; i++)
{
    Object o = new Object();
    v.add(o);
    o = null;
}//此時,所有的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的內存泄露表現為一個內存對象的生命周期超出了程序需要它的時間長度。我們有時也將其稱為“對象游離”。

例如:

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變量,與類實例生命周期相同的要聲明為實例變量……以此類推。第二,在內存對象不再需要時,記得手動將其引用置空。

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

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

    之前我們介紹過,JavaGC機制是建立在跟蹤內存的引用機制上的。而在此之前,我們所使用的引用都只是定義一個“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 對象引用的對象已經結束,可供收集了。這使您能夠剛好在對象占用的內存被回收之前采取行動。ReferenceReferenceQueue的配合使用。

GCReferenceReferenceQueue的交互

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

四、注意事項

1GC在一般情況下不會發現軟引用的內存對象,只有在內存明顯不足的時候才會發現并釋放軟引用對象的內存。

2GC對弱引用的發現和釋放也不是立即的,有時需要重復幾次GC,才會發現并釋放弱引用的內存對象。
3、軟引用和弱引用在添加到ReferenceQueue的時候,其指向真實內存的引用已經被置為空了,相關的內存也已經被釋放掉了。而虛引用在添加到ReferenceQueue的時候,內存還沒有釋放,仍然可以對其進行訪問






 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!