讀書筆記——《深入理解Java虛擬機》系列之回收對象算法與四種引用類型

上一篇博客中,博主和大家一起學習了Java虛擬機運行時內存區域的劃分:主要是線程私有的虛擬機棧,本地方法棧和程序計數器以及線程公有的虛擬機堆和方法區。

對於棧內存而言,每個棧幀所需的內存在類結構確定下來後基本已經確定了,棧中的棧幀隨着方法的進入和退出不斷進行入棧和出棧操作,換句話說棧中的內存分配具有確定性,當方法結束時棧中的內存也就自動釋放了。至於程序計數器,它的生命週期與線程的生命週期相同,在線程結束時,內存也就自動釋放了。虛擬機堆內存和方法區與它們並不相同,因爲我們只有在程序運行時才知道會創建哪些對象,class的加載是在運行時的,還有一些class是通過動態代理在運行時生成的,因此兩個內存區域的內存分配和回收都是動態的。Java的垃圾回收主要關注的就是這兩塊內存區域。

1.需要被回收的對象

在考慮Java虛擬機的垃圾回收之前,最重要的一點就是判斷哪些對象應該被回收。

1.1引用計數法

引用計數法通過給每個對象添加一個引用計數器,每當一個地方引用它時,它的計數器數值加1;當引用失效的話,計數器數值減1;任何時刻計數器數值爲0的對象就是應該被回收的對象。這種算法簡單實用,但是卻很難解決對象之間的相互引用問題,如下所示:

public class ReferenceCountingGC{
  public Object instance = null;

  public static void main(){
    ReferenceCountingGC objA = new ReferenceCountingGC();   
    ReferenceCountingGC objB = new ReferenceCountingGC();
    objA.instance = objB;
    objB.instance = objA;
    objA = null;
    objB = null;

    //在這裏通知虛擬機可以進行gc
    System.gc();
  }

}

上面的代碼在虛擬機內存中以下圖的方式所呈現:
6e7f07ad158e40759bc24cfaa3ad4a28.png
儘管我們在代碼中將objA和objB指向了null,由於這兩個對象互相引用,它們的引用計數器的數值仍然不爲0,因此若使用這種引用計數法對象X和對象Y都沒有辦法被回收。但如果我們查看Java gc的日誌,我們會發現,這兩個對象的內存已經被回收了,這是因爲Java採用了另一種算法來判斷哪些對象該被回收。

1.2可達性分析算法

Java的GC機制是根據可達性分析算法(Reachability Analysis)來判定對象是否存活的。簡單來說,這個算法通過一系列的“GC Roots”的對象作爲起始點,從這些起始點開始向下搜索,搜索走過的路叫做引用鏈,當一個對象到GC Roots沒有任何引用鏈時,證明此對象不可達,如下圖中的object5,object6,object7:
605c45a5d89e4408bec78a0d2937b4cd-ReachabilityAnalysis.jpg

那麼在Java中哪些對象可以用被當作GC Roots呢?

  • 在虛擬機棧(棧幀中的局部變量表)中的對象引用:就像我們上面代碼中的例子,起初在main方法中聲明的兩個對象引用objA和objB分別指向堆內存中的對象X和對象Y,一旦我們將objA和objB兩個局部變量引用指向了null,對象X和對象Y即使互相引用但對於原本的GC Roots(objA和objB)而言已經是不可達了,因此這兩個對象所佔的內存可以被回收,而不會像引用計數法,由於兩個對象的計數器數值不爲0導致不能被回收。
  • 在方法區中類的靜態的對象引用:這些由static標識的靜態變量引用也會被當作GC的根節點。
  • 在方法區中常量的對象引用:每個類都擁有一個常量池,這些常量池中也有一些對象的引用,這些常量引用也會被當作GC的根節點。
  • 在本地方法棧中持有的對象引用:一些對象在被傳入到本地方法前,這些對象還沒有被釋放,此時這些對象引用也會被當作GC的根節點。
  • 方法區中類的Class對象引用:每個類被JVM加載時都會創建一個代表這個類的唯一的Class類型的對象,這個Class對象同樣存放在堆中,當這個類不再被使用時,方法區中的類數據和在堆中的Class對象都需要被回收。因此,Class對象的引用也會被當作GC的根節點。

2.Java中的引用

無論是通過上述那種算法判斷對象是否應該被回收,都和對象是否被引用相關。

在Java中,存在四種引用類型:

  • 強引用(Strong Reference):強引用是Java中實例化對象採用的默認的引用類型,如“Object o = new Object()”這類的強引用,只要強引用存在,垃圾收集器就不會回收掉被引用的對象。
  • 軟引用(Soft Reference):軟引用用來描述一些還有用但並非必需的對象。對於軟引用指向的對象,在系統將要發生內存溢出之前,會將這些對象所佔的內存回收,如果回收之後仍然沒有足夠的內存,纔會拋出內存溢出異常。
    SoftReference sr = new SoftReference(new String("hello"));
    System.out.println(sr.get());`
  • 弱引用(Weak Reference):弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用指向的對象只能生存到下一次垃圾收集發生之前,一旦垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉被弱引用指向的對象。
    WeakReference<String> wr = new WeakReference<String>(new String("hello"));
        System.out.println(wr.get());
        System.gc();                //通知JVM的gc進行垃圾回收
        System.out.println(wr.get());
  • 虛引用(Phantom Reference):虛引用是最弱的一種引用關係。一個對象是否有虛引用對它是否會被回收完全沒有影響,我們甚至不能通過虛引用來獲得一個對象的實例。設置虛引用的唯一用處就是當該對象被回收時會收到一個系統通知。

下面博主給大家寫一個,關於各種引用類型與內存回收的一個綜合例子,來幫助大家更好地理解不同引用類型與垃圾回收的關係:

package com.wxueyuan.test;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

public class ReferTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        if(args.length!=0) {
            switch(args[0]) {
                case "strong" :
                    strongReferenceTest();
                    break;
                case "soft":
                    softReferenceTest();
                    break;
                case "weak":
                    weakReferenceTest();
                    break;
                case "phantom":
                    phantomReferenceTest();
                    break;
            }
        }
    }

    public static void strongReferenceTest() {
        List<ReferObject> list = new ArrayList<>();
        for(Integer i =1; i<=10; i++) {
            //實例化ReferObject
            ReferObject obj = new ReferObject(i.toString());
            //將對象放入list中防止被垃圾回收
            list.add(obj);
            System.out.println(obj);
        }
    }

    public static void softReferenceTest() {
        SoftReference<ReferObject> sr = new SoftReference<ReferObject>(new ReferObject("obj"));
        System.out.println(sr.get());
        //通知jvm可以進行垃圾回收
        System.gc();
        //等待gc工作
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("after gc worked " +sr.get());
        System.out.println("***************************");

        List<SoftReference<ReferObject>> list = new ArrayList<>();
        for(Integer i =1; i<=10; i++) {
            ReferObject obj = new ReferObject(i.toString());
            list.add(new SoftReference<ReferObject>(obj) );
            System.out.println(list.get(i-1).get());
            //每隔2s創建一個對象
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    public static void weakReferenceTest() {
        WeakReference<ReferObject> wr = new WeakReference<ReferObject>(new ReferObject("obj"));
        //通知jvm可以進行垃圾回收
        System.out.println(wr.get());
        //等待gc工作
        System.gc();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("after gc worked " +wr.get());
        System.out.println("***************************");

        List<WeakReference<ReferObject>> list = new ArrayList<>();
        for(Integer i =1; i<=10; i++) {
            ReferObject obj = new ReferObject(i.toString());
            list.add(new WeakReference<ReferObject>(obj) );

            //每隔2s創建一個對象
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(list.get(i-1).get());
        }
    }

    public static void phantomReferenceTest() {
        ReferObject obj = new ReferObject("obj");
        PhantomReference<ReferObject> phantomReference = new PhantomReference<ReferObject>(obj, new ReferenceQueue<>()); 
        System.out.println(phantomReference.get());
        //查看對象是否不在內存中
        System.out.println(phantomReference.isEnqueued());

        obj=null;
        //通知gc工作
        System.gc();
        //等待gc工作
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //查看對象是否不在內存中
        System.out.println(phantomReference.isEnqueued());
        //通知gc工作
        System.gc();
        //等待gc工作
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //查看對象是否不在內存中
        System.out.println(phantomReference.isEnqueued());
    }


}

 class ReferObject{
    private String id;
    //用來增大每個對象所佔內存大小
    private double[] d = new double[30000]; 

    public ReferObject(String id) {
        this.id = id;
        System.out.println("create ReferObject "+id);
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return "object "+this.id;
    }

    @Override
    protected void finalize() throws Throwable {
        // TODO Auto-generated method stub
        System.out.println("finalize method executed for object "+this.id);
    }
}

這個例子可能有點長,博主會將它拆分開來講,運行這個程序的虛擬機參數如下,用來限制堆內存大小:

java -Xmx1m -Xms1m

根據運行程序參數的不同strong,soft,weak,phantom分別代表着運行strongReferenceTest(),softReferenceTest(),weakReferenceTest()和phantomReferenceTest()這四個方法。下面博主就來詳細解釋下這四個方法和它們的運行結果。

public static void strongReferenceTest() {
        List<ReferObject> list = new ArrayList<>();
        for(Integer i =1; i<=10; i++) {
            //實例化ReferObject
            ReferObject obj = new ReferObject(i.toString());
            //將對象放入list中防止被垃圾回收
            list.add(obj);
            System.out.println(obj);
        }
}

運行結果爲:
create ReferObject 1
object 1
create ReferObject 2
object 2
create ReferObject 3
object 3
create ReferObject 4
object 4
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at com.wxueyuan.test.ReferObject.(ReferTest.java:142)
at com.wxueyuan.test.ReferTest.strongReferenceTest(ReferTest.java:36)
at com.wxueyuan.test.ReferTest.main(ReferTest.java:17)

很明顯由於我們限制了虛擬機堆內存的大小爲1M,在我們建立了幾個大的對象ReferObject之後,就堆內存溢出了。這個例子是想要告訴大家,我們通常情況下建立的對象都屬於強引用,也就意味着虛擬機即使拋出內存異常也不會嘗試去回收這些重要的對象。

public static void softReferenceTest() {
        SoftReference<ReferObject> sr = new SoftReference<ReferObject>(new ReferObject("obj"));
        System.out.println(sr.get());
        //通知jvm可以進行垃圾回收
        System.gc();
        //等待gc工作
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("after gc worked " +sr.get());
        System.out.println("***************************");

        List<SoftReference<ReferObject>> list = new ArrayList<>();
        for(Integer i =1; i<=10; i++) {
            ReferObject obj = new ReferObject(i.toString());
            list.add(new SoftReference<ReferObject>(obj) );
            System.out.println(list.get(i-1).get());
            //每隔2s創建一個對象
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
}

運行結果爲
create ReferObject obj
object obj
after gc worked object obj

create ReferObject 1
object 1
create ReferObject 2
object 2
finalize method executed for object obj
finalize method executed for object 2
finalize method executed for object 1
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at com.wxueyuan.test.ReferObject.(ReferTest.java:142)
at com.wxueyuan.test.ReferTest.softReferenceTest(ReferTest.java:60)
at com.wxueyuan.test.ReferTest.main(ReferTest.java:20)

我們先分析這個程序輸出星號之前的部分,我們對一個referObject建立軟引用,然後通過它的get方法獲取了它引用的對象obj,之後我們通知GC工作並等待了2s,但事實上我們發現Java GC並沒有回收掉這個對象,因爲我們重寫了對象的finalize()方法,如果GC回收該對象,則finalize()方法會被調用,此後我們再次調用sr.get(),果然還能夠獲取到它引用的對象。

接下來我們看星號之後的部分,我們用for循環去大量創建referObject,就在堆內存即將溢出之前,我們看到三個對象的finalize()方法都被調用了,說明虛擬機會在內存溢出之前才嘗試回收掉軟引用引用的對象。

public static void weakReferenceTest() {
        WeakReference<ReferObject> wr = new WeakReference<ReferObject>(new ReferObject("obj"));
        //通知jvm可以進行垃圾回收
        System.out.println(wr.get());
        //等待gc工作
        System.gc();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("after gc worked " +wr.get());
        System.out.println("***************************");

        List<WeakReference<ReferObject>> list = new ArrayList<>();
        for(Integer i =1; i<=10; i++) {
            ReferObject obj = new ReferObject(i.toString());
            list.add(new WeakReference<ReferObject>(obj) );

            //每隔2s創建一個對象
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(list.get(i-1).get());
        }
}

運行結果爲:
create ReferObject obj
object obj
finalize method executed for object obj
after gc worked null

create ReferObject 1
object 1
create ReferObject 2
object 2
create ReferObject 3
finalize method executed for object 2
finalize method executed for object 1
object 3
finalize method executed for object 3
create ReferObject 4
object 4
create ReferObject 5
object 5
create ReferObject 6
finalize method executed for object 4
finalize method executed for object 5
object 6
create ReferObject 7
finalize method executed for object 6
object 7
create ReferObject 8
object 8
create ReferObject 9
finalize method executed for object 7
finalize method executed for object 8
object 9
finalize method executed for object 9
create ReferObject 10
object 10

同樣我們也先分析這個程序輸出星號之前的部分,我們首先建立referObject的弱引用,然後通過它的get()方法獲得它指向的對象,之後我們通知GC進行垃圾回收,我們可以看到obj對象的finalize()方法被調用,同時弱引用的get()方法無法再獲得它原本指向的對象了。

接下來我們看星號之後的部分,我們用for循環去大量創建referObject,但由於每個對象創建之間都間隔了兩秒鐘,因此Java GC在每次發現對象不可達之後就自動將對象所佔的內存回收了,因此這個程序並不會內存溢出,同時我們也發現每個對象的finalize()方法都被調用了,說明每個被弱引用指向的對象只能存活到下一次垃圾回收之前。

public static void phantomReferenceTest() {
        ReferObject obj = new ReferObject("obj");
        PhantomReference<ReferObject> phantomReference = new PhantomReference<ReferObject>(obj, new ReferenceQueue<>()); 
        System.out.println(phantomReference.get());
        //查看對象是否不在內存中
        System.out.println(phantomReference.isEnqueued());

        obj=null;
        //通知gc工作
        System.gc();
        //等待gc工作
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //查看對象是否不在內存中
        System.out.println(phantomReference.isEnqueued());
        //通知gc工作
        System.gc();
        //等待gc工作
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //查看對象是否不在內存中
        System.out.println(phantomReference.isEnqueued());
}

這個程序的運行結果如下:
create ReferObject obj
null
false
finalize method executed for object obj
false
true

虛引用的實例化需要兩個參數,一個是引用的對象的實例,另一個就是ReferenceQueue的實例了,上面的代碼首先通過虛引用的get()方法獲取引用實例,但是返回是null,說明我們並不能通過虛引用獲取對象實例。虛引用的isEnqueued()方法可以用來告訴我們對象是否已經不在內存中,第一次查看時,對象實例並沒有被gc回收因此返回false;之後我們將obj指向null,同時通知GC工作並等待2s,我們發現對象的finalize()方法執行了,但是虛引用的isEnqueued()方法依舊返回false,直到我們再次通知GC工作之後,虛引用的isEnqueued()方法才返回true,說明對象已經不再內存中了。這其實是因爲對於重寫了finalize()方法的對象而言,GC在發現該對象不可達後,會首先將它標記並放入F-Queue隊列中,當GC再次工作時會將F-Queue中需要被回收的對象回收掉,因此在我們的例子中,第二次System.gc()調用後,對象才真正被回收了。

3.死而復生的對象

利用重寫finalize()方法的特點,我們可以成功地將一個對象從被回收的邊緣拯救回來,但是這種方法是及其不推薦的。在這裏博主提供這個例子只是爲了讓大家更好地理解GC要回收一個對象時發生的事情。

public class ReviveObject
{
    public static ReviveObject obj=null;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed for object ");
        //又將obj指向了當前對象實例
        obj = this;
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new ReviveObject();
        System.out.println(obj);
        obj = null; //將obj設爲null
        //通知gc工作
        System.out.println("let GC do its work");
        System.gc();
        Thread.sleep(1000);
        if(obj == null) {
            System.out.println("obj is null");
        } else {
            System.out.println("obj is alive");
            System.out.println(obj);
        }

        obj = null;//由於obj被複活,此處再次將obj設爲null
        System.out.println("let GC do its work again");
        System.gc();
        Thread.sleep(1000);
        if(obj == null) {
            //對象的finalize方法僅僅會被調用一次,所以當GC再次檢測到對象不可達時,obj會直接被GC回收
            System.out.println("obj is null");
        } else {
            System.out.println("obj is alive");
        }
    }

}

運行結果大家應該也很好預料了:

com.wxueyuan.test.ReviveObject@15db9742
let GC do its work
finalize method executed for object
obj is alive
com.wxueyuan.test.ReviveObject@15db9742
let GC do its work again
obj is null

obj對象的finalize()方法在第一次GC工作時觸發了,並且重新將obj指向了原來的實例,當第二次將obj指向null並通知GC工作後,GC將直接回收掉已經觸發過finalize()方法的對象,因此第二次會發現obj = null了。

在本節博客中,博主與大家一起了解了兩種判定對象是否應該回收的算法:引用計數法和可達性分析算法。兩種算法各有好處,但是在Java中我們使用的時可達性分析算法。同時博主也提供了一個例子幫助大家瞭解Java中的四種引用類型及其特點。至於finalize()方法,它的運行代價很高,不確定性大,它並不適合用來做釋放資源的工作,博主在這裏給出的例子只是爲了方便大家理解GC工作時,finalize()方法被觸發的條件僅此而已。如果大家對本篇博客中提到的Reference和ReferenceQueue感興趣,歡迎大家觀看博主的另一篇文章。那我們下篇博客見了~。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章