1. 程式人生 > >JVM——從簡單Demo分析Java 四種引用型別

JVM——從簡單Demo分析Java 四種引用型別

有記憶體分配自然也就有記憶體回收,是否回收一個物件就需要判斷它是否符合回收條件。

判斷物件是否存活主要也就是引用計數法以及可達性分析演算法。這兩種演算法可以參考:JVM——引用計數演算法與可達性分析演算法

但是無論是通過引用計數演算法判斷物件的引用數量,還是通過可達性分析演算法判斷物件的引用鏈是否可達,判定物件是否存活都與“引用”有關。

在JDK1.2以前,Java中的引用的定義很傳統:如果reference型別的資料中儲存的數值代表的是另一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。

這種定義很純粹,也很粗糙。作為一個新時代的有志青年,我們當然希望JDK可以能夠分辨出更多種類的物件(就像女朋友希望我們能夠多分辨一些口紅的色號那樣)。

JDK自然也知道我們的想法,因此在JDK1.2之後,Java就對引用的概念進行了擴充,將引用分為了強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種。

而這四種引用型別,就是我們今天的主菜。我們今天就從JVM原始碼的角度,來分析分析Java的引用型別。

1.強引用

強引用就是指在程式程式碼中普遍存在的,類似於“Object obj=new Object()”這一類的引用。如果一個物件具有強引用,那垃圾回收器絕不會回收它。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足的問題。 

強引用的特性如下:

  • 強引用可以直接訪問目標物件。
  • 強引用所指向的物件在任何時候都不會被系統回收。
  • 強引用可能導致記憶體洩漏。

強引用大家都熟悉,這裡就不舉例啦~

2.軟引用

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

對於軟引用關聯著的物件,如果記憶體充足,則垃圾回收器不會回收該物件,如果記憶體不夠了,就會回收這些物件的記憶體。

在JDK 1.2 之後,提供了 SoftReference 類

來實現軟引用。軟引用可用來實現記憶體敏感的快取記憶體。

軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。

注意:Java 垃圾回收器準備對SoftReference所指向的物件進行回收時,呼叫物件的 finalize() 方法之前,SoftReference物件自身會被加入到這個 ReferenceQueue 物件中,此時可以通過 ReferenceQueue 的 poll() 方法取到它們。

請看下面的程式碼Demo:

/**
 * 軟引用:對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收( 因為是在第一次回收後才會發現記憶體依舊不充足,才有了這第二次回收 )。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。
 * 對於軟引用關聯著的物件,如果記憶體充足,則垃圾回收器不會回收該物件,如果記憶體不夠了,就會回收這些物件的記憶體。
 * 通過debug發現,軟引用在pending狀態時,referent就已經是null了。
 *
 * 啟動引數:-Xmx5m
 *
 */
public class SoftReferenceDemo {
    private static ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(3000);
        MyObject object = new MyObject();
        SoftReference<MyObject> softRef = new SoftReference(object, queue);
        new Thread(new CheckRefQueue()).start();
        object = null;
        System.gc();
        System.out.println("After GC : Soft Get = " + softRef.get());
        System.out.println("分配大塊記憶體");

        /**
         * ====================== 控制檯列印 ======================
         * After GC : Soft Get = I am MyObject.
         * 分配大塊記憶體
         * MyObject's finalize called
         * Object for softReference is null
         * After new byte[] : Soft Get = null
         * ====================== 控制檯列印 ======================
         *
         * 總共觸發了 3 次 full gc。第一次有System.gc();觸發;第二次在在分配new byte[5*1024*740]時觸發,然後發現記憶體不夠,於是將softRef列入回收返回,接著進行了第三次full gc。
         */
//        byte[] b = new byte[5*1024*740];

        /**
         * ====================== 控制檯列印 ======================
         * After GC : Soft Get = I am MyObject.
         * 分配大塊記憶體
         * Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
         *      at com.bayern.multi_thread.part5.SoftReferenceDemo.main(SoftReferenceDemo.java:21)
         * MyObject's finalize called
         * Object for softReference is null
         * ====================== 控制檯列印 ======================
         *
         * 也是觸發了 3 次 full gc。第一次有System.gc();觸發;第二次在在分配new byte[5*1024*740]時觸發,然後發現記憶體不夠,於是將softRef列入回收返回,接著進行了第三次full gc。當第三次 full gc 後發現記憶體依舊不夠用於分配new byte[5*1024*740],則就丟擲了OutOfMemoryError異常。
         */
        byte[] b = new byte[5*1024*790];

        System.out.println("After new byte[] : Soft Get = " + softRef.get());
    }

    public static class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;
        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>) queue.remove();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (obj != null) {
                System.out.println("Object for softReference is " + obj.get());
            }
        }
    }

    public static class MyObject {
        @Override
        protected void finalize() throws Throwable {
            System.out.println("MyObject's finalize called");
            super.finalize();
        }
        @Override
        public String toString() {
            return "I am MyObject.";
        }
    }
}

 3.弱引用

弱引用也是用來描述非必須的物件,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。一旦一個弱引用物件被垃圾回收器回收,便會加入到一個註冊引用佇列中。在JDK1.2之後,提供了WeakReference類來實現弱引用。

請看Demo:


public class WeakReferenceDemo {

    private static ReferenceQueue<MyObject> queue = new ReferenceQueue<>();

    public static void main(String[] args) {
        MyObject object = new MyObject();
        Reference<MyObject> weakRef = new WeakReference<>(object, queue);
        System.out.println("建立的弱引用為 : " + weakRef);
        new Thread(new CheckRefQueue()).start();
        object = null;
        System.out.println("Before GC: Weak Get = " + weakRef.get());
        System.gc();
        System.out.println("After GC: Weak Get = " + weakRef.get());
    }

    public static class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;
        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>)queue.remove();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(obj != null) {
                System.out.println("刪除的弱引用為 : " + obj + " , 獲取到的弱引用的物件為 : " + obj.get());
            }
        }
    }

    public static class MyObject {
        @Override
        protected void finalize() throws Throwable {
            System.out.println("MyObject's finalize called");
            super.finalize();
        }
        @Override
        public String toString() {
            return "I am MyObject";
        }
    }
}

執行結果如下:

注意:Java 垃圾回收器準備對WeakReference所指向的物件進行回收時,呼叫物件的 finalize() 方法之前,WeakReference物件自身會被加入到這個 ReferenceQueue 物件中,此時可以通過 ReferenceQueue 的 poll() 方法取到它們。

4.虛引用

虛引用也稱作幽靈引用或者幻影引用,至於為什麼取這樣的名字,那自然是因為它太弱啦。在這四種引用關係中,虛引用是最弱的一種引用關係。它弱到一個物件是否有虛引用的存在,完全不會對其生存時間構成影響。且不同於軟引用和弱引用,虛引用無法通過 get() 方法來取得目標物件的強引用從而使用目標物件,觀察原始碼可以發現 get() 被重寫為永遠返回 null

既然虛引用這麼弱,那還要虛引用做什麼呢?

為一個物件設定虛引用關聯的唯一目的就是能夠在這個物件被收集器回收的時候收到一個系統通知(即跟蹤物件的垃圾回收)。

換句話說,其實虛引用主要被用來跟蹤物件被垃圾回收的狀態,通過檢視引用佇列中是否包含物件所對應的虛引用來判斷它是否 即將被垃圾回收,從而採取行動。它並不被期待用來取得目標物件的引用,而目標物件被回收前,它的引用會被放入一個 ReferenceQueue 物件中,從而達到跟蹤物件垃圾回收的作用。當phantomReference被放入佇列時,說明referent的finalize()方法已經呼叫,並且垃圾收集器準備回收它的記憶體了。

注意:對於Phantom Reference 來說,只有在當 Java 垃圾回收器對其所指向的物件真正進行回收時,才會將其加入到這個 ReferenceQueue 物件中,這樣就可以追綜物件的銷燬情況。由於這個時候referent物件的finalize()方法已經呼叫過了。所以具體用法和之前兩個有所不同,它必須傳入一個 ReferenceQueue 物件。當虛引用所引用物件準備被垃圾回收時,虛引用會被新增到這個佇列中。

請看Demo1:

public class PhantomReferenceDemo {
    private static ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
    public static void main(String[] args) throws InterruptedException {
        MyObject object = new MyObject();
        Reference<MyObject> phanRef = new PhantomReference<>(object, queue);
        System.out.println("建立的虛擬引用為 : " + phanRef);
        new Thread(new CheckRefQueue()).start();
        object = null;
        int i = 1;
        while (true) {
            System.out.println("第" + i++ + "次GC");
            System.gc();
            TimeUnit.SECONDS.sleep(1);
        }
    }
    public static class MyObject {
        @Override
        protected void finalize() throws Throwable {
            System.out.println("MyObject's finalize called");
            super.finalize();
        }
        @Override
        public String toString() {
            return "I am MyObject";
        }
    }

    public static  class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;
        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>)queue.remove();
                System.out.println("刪除的虛引用為: " + obj + " , 獲取虛引用的物件 : " + obj.get());
                System.exit(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在這個程式中,在經過一次GC之後,系統找到了垃圾物件,並呼叫finalize()方法回收記憶體,但沒有立即加入PhantomReference Queue中。
因為MyObject物件重寫了finalize()方法,並且該方法是一個非空實現,所以這裡MyObject也是一個Final Reference。所以第一次GC完成的是Final Reference的事情。
而第二次GC時,該物件(即MyObject)物件會真正被垃圾回收器進行回收,此時,將PhantomReference加入虛引用佇列( PhantomReference Queue )。
而且每次gc之間需要停頓一些時間,已給JVM足夠的處理時間;如果這裡沒有TimeUnit.SECONDS.sleep(1); 可能需要gc到第5、6次才會成功。 

執行結果如下:

 

Demo2:

public class PhantomReferenceDemo2 {

    public static void main(String[] args) {
        ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
        MyObject object = new MyObject();
        Reference<MyObject> phanRef = new PhantomReference<>(object, queue);
        System.out.println("建立的虛擬引用為 : " + phanRef);
        object = null;
        System.out.println(phanRef.get());
        System.gc();
        System.out.println("referent : " + phanRef);
        System.out.println(queue.poll() == phanRef); //true
    }

    public static class MyObject {
        @Override
        public String toString() {
            return "I am MyObject";
        }
    }
}

這個程式與上面這個demo1不同,這裡因為MyObject沒有重寫finalize()方法,所以這裡的在System.gc()後就會處理PhantomReference加入到PhantomReference Queue中。

程式執行結果為:

 

好啦,以上就是關於Java四種引用型別的相關知識總結啦,如果大家有什麼不明白的地方或者發現文中有描述不好的地方,歡迎大家留言評論,我們一起學習呀。

 

Biu~~~~~~~~~~~~~~~~~~~~宫å´éªé¾ç«è¡¨æå|é¾ç«gifå¾è¡¨æåä¸è½½å¾ç~~~~~~~~~~~~~~~~~~~~~~pia!

參考文章:https://www.jianshu.com/p/9a089a37f78d