天天看點

4 Java 記憶體回收及算法 — 引用及記憶體洩漏

本節要點

  • Java 引用
  • Java 記憶體洩漏的原因
  • 垃圾回收機制的基本算法
  • 堆記憶體的分代回收
  • 記憶體管理小技巧

問題

1、JVM 在什麼時候決定回收一個對象所占據的記憶體?

當一個 Java 對象失去引用時,JVM 的垃圾回收機制會自動清除對象,回收其所占用的記憶體空間。

也就是說,當 Java 對象被建立後,垃圾回收機制會實時的監控每個對象的運作狀态,包括對象的空間申請、引用、被引用、指派等。當監控到某個對象不再被引用變量所引用時,回收機制就會在回收時回收該對象所占用的空間。

2、JVM 會不會漏掉回收某些 Java 對象,由此造成記憶體洩漏?

不再使用的強引用沒有置空時。

4.1 Java 引用的種類

Java 記憶體管理包括記憶體配置設定(建立 Java 對象時)和記憶體回收(回收 Java 對象時)。配置設定和回收任務都由 JVM 自動完成,是以也加重了 JVM 的工作,進而使 Java 程式運作較慢。

當通過關鍵字 new 建立對象時,即為 Java 對象申請記憶體空間,JVM 會在堆記憶體中為每個對象配置設定空間;當一個 Java 對象失去引用時,JVM 的垃圾回收機制會自動清除對象,回收其所占用的記憶體空間。

4.1.1 對象在記憶體中的狀态

可以把 JVM 記憶體中對象引用了解成一種有向圖,因為 Java 所有對象都是由一條一條線程建立出來的,是以可以把線程對象當成有向圖的起始頂點。

例如,對于單線程,整個程式隻有一條 main 線程,那麼該圖就是以 main 線程為頂點的有向圖,main 頂點可達的對象都處于可達狀态,不會被回收掉;如果某個對象在有向圖中處于不可達狀态,即該對象不再被引用,接下來會被垃圾回收機器所回收。

public static void main(String[] args){
    Node n1 = new Node("第一個節點");
    Node n2 = new Node("第二個節點");
    Node n3 = new Node("第三個節點");

    n1.next = n2;
    n3 = n2;
    n2 = null;
}
           
4 Java 記憶體回收及算法 — 引用及記憶體洩漏

當一個對象在堆記憶體中運作時,對應有向圖中的狀态,可以把對象所處的狀态分為如下3種:

1) 可達狀态:有一個以上的引用變量引用它。在有向圖中可以從起始頂點導航到該對象,程式可通過引用變量來調用該對象的屬性和方法,那該對象就處于可達狀态。

2) 可恢複狀态:若程式中某個對象不再有任何引用變量引用它,它将先進入可恢複狀态,此時從有向圖的起始頂點不能導航到該對象,系統的垃圾回收器準備回收該對象所占用的記憶體。

       在回收該對象之前,系統會調用可恢複狀态的對象的 finalize() 方法進行資源清理,如果 finalize() 方法重新讓一個以上引用變量引用該對象,則該對象會再次變為可達狀态;否則,将進入不可達狀态。如:

public void test(){
    String a = new String("aaa"); // ①
    a = new String("bbb"); // ②
}
           

當程式執行了 ② 代碼後,字元串 “aaa” 對象處于可恢複狀态,而字元串 “bbb” 對象處于可達狀态。

3) 不可達狀态:當對象的所有關聯都被切斷,且系統調用對象的 finalize() 方法依然沒有使該對象變成可達狀态,那該對象将永久性地失去引用,最後變成不可達狀态。

采用有向圖方式管理記憶體中的對象,可以友善的解決循環引用導緻對象不能被回收的問題,如,有3個對象互相引用,A 引用 B,B 引用 C,C 又引用 A 對象,這樣三者都沒有失去引用,但從有向圖的起始頂點(即程序根)不可到達它們,垃圾回收器就會回收它們。

采用有向圖來管理記憶體中的對象具有精度高的優點,缺點是效率較低。

4.1.2 四種引用類型

為了更好的管理對象引用,從 JDK 1.2 開始,Java 在 java.lang.ref 包下提供了3個類:SoftReference、PhantomReference、WeekReference,即軟引用、虛引用和弱引用。

Java 對對象的引用有四種:強引用、軟引用、虛引用和弱引用。

1. 強引用

程式建立一個對象,并把該對象賦給一個引用變量,該引用變量就是強引用。

當對象被強引用變量所引用時,它處于可達狀态,不會被回收。由于 JVM 肯定不會回收強引用所引用的 Java 對象,是以強引用是造成 Java 記憶體洩漏的主要原因之一。

2. 軟引用

軟引用通過 SoftReference 類來實作,對于軟引用對象,當系統記憶體空間足夠時,它不會被系統回收;當系統記憶體空間不足時,系統将會回收該對象。

軟引用通常用于對記憶體敏感的程式中,軟引用是強引用很好的替代。

當需要大量建立某個類的新對象,而且有可能重新通路已建立老對象時,可以充分使用軟引用來解決記憶體緊張的問題。

SoftReference<Person>[] people = new SoftReference[];
for (int i = ; i < people.length; i++ ){
    Person people = new Person("name"+i, i);
    people[i] = new SoftReference<Person>(people);
}

System.out.println("回收前:"+people[].get());
// 通知系統進行垃圾回收
System.gc();
System.out.println("回收後:"+people[].get());
           

運作後輸出:

回收前:Person[name=name0,age=0]
回收後:Person[name=name0,age=0]
           

上面程式建立了一個長度為 100 的 SoftReference 數組,當系統記憶體足夠時,即使進行垃圾回收,垃圾回收器也不會回收這些 Person 對所占用的記憶體空間。

通過指令行指定最大記憶體為 2M 後(1M 啟動不了 JVM),再執行 class 檔案結果如下:

E:\JavaWeb_workspaces\dbBackup\src\com\zxk\test>javac -encoding utf-8 -d . SoftReferenceTest.java

E:\JavaWeb_workspaces\dbBackup\src\com\zxk\test>java -Xmx2m -Xms2m com/zxk/test/SoftReferenceTest
回收前:null
回收後:null
           

當使用 -Xmx2m 參數設定最大記憶體隻有 2M 時,而建立一個長度為 100000 的數組會造成系統記憶體資源緊張,這種情況下,軟引用所引用的 Java 對象将會被回收。

另外,最大堆記憶體不要設定為 1M,啟動不了 JVM:

4 Java 記憶體回收及算法 — 引用及記憶體洩漏

3. 弱引用

弱引用所引用對象的生存期更短,通過 WeakReference 類實作。對于隻有弱引用的對象而言,當系統垃圾回收器運作時,不管系統記憶體是否足夠,總會回收該對象所占用的記憶體。

public class WeakReferenceTest {
    public static void main(String[] args){
        String str = new String("瘋狂Java講義");
        // 建立一個弱引用,讓其引用到字元串
        WeakReference<String> wr = new WeakReference<String>(str); // ①
        // 斷開強引用
        str = null; // ②
        System.out.println("回收前:"+wr.get()); // ③

        System.gc();
        System.out.println("回收後:"+wr.get());
    }
}
           

執行結果:

回收前:瘋狂Java講義
回收後:null
           

程式中,建立了一個弱引用對象,并讓該對象和 str 變量引用同一個對象,然後在 ② 代碼中切斷 str 對字元串的強引用,此時引用圖如下:

4 Java 記憶體回收及算法 — 引用及記憶體洩漏

此時該字元串隻有一個弱引用對象引用它,程式依然可以通過這個弱引用對象來通路該字元串常量,所有③輸出“回收前:瘋狂Java講義”,然後程式調用 gc(),強制垃圾回收,隻有弱引用的對象被清理掉,輸出為 null,表明該對象被清理了。

注意:該測試程式建立字元串對象時,不要使用 String str = “瘋狂Java講義”; ,這樣将看不到效果,因為采用常量值定義字元串時,系統會緩存這個字元串常量(會使用強引用來引用它),而系統不會回收被緩存的字元串常量。

由于弱引用具有這種因為随時會被回收掉的不确定性,是以程式擷取弱引用所引用的對象必須小心空指針異常。若程式需要使用該對象,應先判空,然後确定是否重新建立該對象。

// 取出弱引用所引用的對象
obj = wr.get();
if(obj == null){
    // 重新建立對象,建議先使用強引用來引用它(考慮到回收時機的不确定性)
    obj = recreateObj(); // 在使用前不會被回收
    // 加上弱引用
    wr = new WeakReference(obj);
}
... // 操作 obj 

// 切斷強引用,以便回收
obj = null;
           

與 WeakReference 功能類似的還有 WeakHashMap(與HashMap的用法類似)。實際中很少會直接使用 WeakReference 來引用某個 Java 對象,因為這種時候系統記憶體往往不會特别緊張。當程式有大量的對象需要使用弱引用來引用時,可以考慮使用 WeakHashMap 來儲存這些對象。

在垃圾回收器運作之前,WeakHashMap 的功能與普通 HashMap 功能完全相似,但垃圾回收器運作時,WeakHashMap 中所有的 key - value 對都會被清空,除非某些 key 還有強引用在引用它們。

4. 虛引用

虛引用通過 PhantomReference 類實作,該引用對對象本身沒有太大影響,完全類似于沒有引用。如果一個對象隻有一個虛引用,那和沒有引用的效果大緻相同。

虛引用的主要用于跟蹤對象被垃圾回收的狀态,程式可以通過檢查與虛引用關聯的引用隊列中是否已經包含指定的虛引用,進而了解虛引用所引用對象是否即将被回收。虛引用不能單獨使用,單獨使用沒有太大的意義,必須和引用隊列(ReferenceQueue)聯合使用。

引用隊列用于儲存被回收後對象的引用。系統回收被引用的對象後,會把被回收對象對應的引用添加到關聯的引用隊列中,這使得可以在對象被回收之前采取行動。

最後,使用這些特殊的引用類,就不能保留對對象的強引用,否則,就會失去這些類所提供的任何好處。

4.2 Java 的記憶體洩漏

程式在運作過程中,系統會不斷的配置設定記憶體空間,那些不再使用的記憶體空間應該及時回收掉,保證系統可以再次使用這些記憶體,若存在無用的記憶體沒有被回收,那就是記憶體洩漏。

對于 C++ 程式,若程式員忘了回收無用對象,即産生記憶體洩漏;

對于 Java 程式,若程式中有一些對象處于可達狀态,即使不會再被引用,它們所占用的記憶體空間也不會被回收,它們所占用的空間也會産生記憶體洩漏。下圖顯示了記憶體洩漏示意圖:

4 Java 記憶體回收及算法 — 引用及記憶體洩漏

如 ArrayList 中 remove(int index)方法代碼中的 elementData[–size] = null; 這句就是為了避免記憶體洩漏的代碼,置空以便垃圾回收器回收。

4 Java 記憶體回收及算法 — 引用及記憶體洩漏

參考資料:

瘋狂Java:突破程式員基本功的16課-Java 的記憶體回收