Java虛擬機器02——物件存活判斷和4種引用

llldddbbb發表於2019-03-31

物件存活判斷

垃圾收集器在對堆進行回收前,第一件事情就是要確定這些物件之中哪些還“存活”著,哪些已經“死去”

引用計數法

給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時候計數器為0的物件是不可能再被使用的。

  • 缺點:難以解決物件之間互相迴圈引用的問題

例子:引用計數演算法的缺陷

-XX:+PrintGCDetails 通過此命令可以列印GC資訊

public class RefrenceCountingGC {
    public Object instance = null;
    private static final int _1MB=1024* 1024;

    private byte[] bigSize = new byte[2 * _1MB];

    public static void testGC() {
        RefrenceCountingGC objA = new RefrenceCountingGC();
        RefrenceCountingGC objB = new RefrenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        // 呼叫GC
        System.gc();
    }

    public static void main(String[] args) {
        testGC();
    }
}
複製程式碼

image.png

上面例子objA與objB互相依賴,從結果來看,記憶體大小從7014k -> 832k,虛擬機器進行了回收,證明虛擬機器不是通過引用計數法來判斷存活的。

可達性分析演算法

通過一系列的成為“GC Roots” 的物件作為起點,從這些節點開始向下搜尋,當一個物件到GC Roots沒有任何GC鏈連線時(從GC Roots到這個物件不可達)則證明這個物件是不可活的。如圖:

image.png

GC Roots的物件包括下面幾種

  • 虛擬機器棧(棧幀中的本地變數)中引用的物件
  • 方法區中類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中JNI(Natice方法)引用的物件

4種引用

經過上面描述得知,物件的存活都與“引用”有關。在JDK1.2之後,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Week Reference)、虛引用(Phantom Reference),引用強度依次減弱

強引用 > 軟引用 > 弱引用 > 虛引用

強引用

是使用最普遍的引用,類似“Object obj = new Object()”。只要強引用還存在,垃圾收集器就不會回收被引用的物件

軟引用

描述一些還有用但並非必須的物件。在系統將要發生記憶體溢位之前,將會把這些物件列進回收範圍之中進行二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。

弱引用

描述非必須物件,強度比軟引用還弱一些。被弱引用關聯的物件只能生存到下次垃圾收集之前。當垃圾收集器工作時,無論當記憶體是否足夠,都會回收掉只被弱引用關聯的物件。

虛引用

一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是在這個物件被收集回收時收到一個系統通知、

4種引用程式碼實踐

判斷物件是否存活

如果通過可達性演算法分析一個物件不可達,此時會被第一次標記,並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋該方法或已經被執行了,則不需要執行finalize()方法。

如果判定需要執行finalize()方法,那這個物件將會放在F-Queue的佇列中,虛擬機器會觸發這個方法,但不承諾會等待它執行結束(如果方法緩慢,將導致崩潰)。稍後GC將會對F-Queue進行第二次標記,並把標記的物件移到“即將回收”的集合中。

綜上,finalize()函式是在JVM回收記憶體時執行的,僅執行一次,但JVM並不保證在回收記憶體時一定會呼叫。

測試引用例子

先建立一個Demo類,重寫它的finallize()方法,如果被GC了,則會列印資訊

public class RefrenceDemo {

    @Override
    protected void finalize() throws Throwable {
        System.out.println("哎呀,我被回收了");
        super.finalize();
    }
}
複製程式碼

測試強引用

測試程式碼:

    public static void main(String[] args) {
        RefrenceDemo demo = new RefrenceDemo();
        System.gc();
    }
複製程式碼

結果: 無任何輸出,則證明強引用物件沒有被回收

測試軟引用

測試程式碼:

VM引數: -Xms20m -Xmx20m

    public static void main(String[] args) {
        List<String> temp = new ArrayList<>();
        SoftReference<RefrenceDemo> ref = new SoftReference<>(new RefrenceDemo());
        for (int i = 0; i < 10000; i++) {
            temp.add(String.valueOf(i));
        }
        System.gc();
    }	
複製程式碼

執行程式碼,此時記憶體充足,並沒有輸出任何結果

將程式碼中的10000改成100000

    public static void main(String[] args) {
        List<String> temp = new ArrayList<>();
        SoftReference<RefrenceDemo> ref = new SoftReference<>(new RefrenceDemo());
        for (int i = 0; i < 100000; i++) {
            temp.add(String.valueOf(i));
        }
        System.gc();
    }
複製程式碼

執行結果:

image.png

此時,finalize()方法被執行了,說名記憶體不足,需要回收軟引用的物件。

測試弱引用

VM引數: -Xms20m -Xmx20m

將上例的10000縮小到1000,此時記憶體空間是足夠的。

     public static void main(String[] args) {
        WeakReference<RefrenceDemo> ref = new WeakReference<>(new RefrenceDemo());

        System.out.println(ref.get());
        
        List<String> temp = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            temp.add(String.valueOf(i));
        }

        System.gc();
    }
複製程式碼

執行結果:

image.png

在GC前,引用還存活,GC後便執行了finalize()方法,說明弱引用只能活到GC前

測試虛引用

VM引數: -Xms20m -Xmx20m

    public static void main(String[] args) {
        ReferenceQueue queue = new ReferenceQueue();
        PhantomReference<RefrenceDemo> ref = new PhantomReference<>(new RefrenceDemo(),queue);

        System.out.println(ref.get());

        System.gc();
    }

複製程式碼

執行結果:

image.png

這說明虛引用在例項化後,就被終止了

相關文章