1. 程式人生 > >你不可不知的Java引用型別之——虛引用

你不可不知的Java引用型別之——虛引用

定義

虛引用是使用PhantomReference建立的引用,虛引用也稱為幽靈引用或者幻影引用,是所有引用型別中最弱的一個。一個物件是否有虛引用的存在,完全不會對其生命週期構成影響,也無法通過虛引用獲得一個物件例項。

說明

虛引用,正如其名,對一個物件而言,這個引用形同虛設,有和沒有一樣。

如果一個物件與GC Roots之間僅存在虛引用,則稱這個物件為虛可達(phantom reachable)物件。

當試圖通過虛引用的get()方法取得強引用時,總是會返回null,並且,虛引用必須和引用佇列一起使用。既然這麼虛,那麼它出現的意義何在??

別慌別慌,自然有它的用處。它的作用在於跟蹤垃圾回收過程,在物件被收集器回收時收到一個系統通知。 當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在垃圾回收後,將這個虛引用加入引用佇列,在其關聯的虛引用出隊前,不會徹底銷燬該物件。 所以可以通過檢查引用佇列中是否有相應的虛引用來判斷物件是否已經被回收了。

如果一個物件沒有強引用和軟引用,對於垃圾回收器而言便是可以被清除的,在清除之前,會呼叫其finalize方法,如果一個物件已經被呼叫過finalize方法但是還沒有被釋放,它就變成了一個虛可達物件。

與軟引用和弱引用不同,顯式使用虛引用可以阻止物件被清除,只有在程式中顯式或者隱式移除這個虛引用時,這個已經執行過finalize方法的物件才會被清除。想要顯式的移除虛引用的話,只需要將其從引用佇列中取出然後扔掉(置為null)即可。

同樣來看一個栗子:

public class PhantomReferenceTest {
    private static final List<Object> TEST_DATA = new LinkedList<>();
    private static final ReferenceQueue<TestClass> QUEUE = new ReferenceQueue<>();

    public static void main(String[] args) {
        TestClass obj = new TestClass("Test");
        PhantomReference<TestClass> phantomReference = new PhantomReference<>(obj, QUEUE);

        // 該執行緒不斷讀取這個虛引用,並不斷往列表裡插入資料,以促使系統早點進行GC
        new Thread(() -> {
            while (true) {
                TEST_DATA.add(new byte[1024 * 100]);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
                System.out.println(phantomReference.get());
            }
        }).start();

        // 這個執行緒不斷讀取引用佇列,當弱引用指向的物件唄回收時,該引用就會被加入到引用佇列中
        new Thread(() -> {
            while (true) {
                Reference<? extends TestClass> poll = QUEUE.poll();
                if (poll != null) {
                    System.out.println("--- 虛引用物件被jvm回收了 ---- " + poll);
                    System.out.println("--- 回收物件 ---- " + poll.get());
                }
            }
        }).start();

        obj = null;

        try {
            Thread.currentThread().join();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    static class TestClass {
        private String name;

        public TestClass(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "TestClass - " + name;
        }
    }
}

使用的虛擬機器設定如下:

-verbose:gc -Xms4m -Xmx4m -Xmn2m

執行結果如下:

[GC (Allocation Failure)  1024K->432K(3584K), 0.0113386 secs]
[GC (Allocation Failure)  1455K->520K(3584K), 0.0133610 secs]
[GC (Allocation Failure)  1544K->648K(3584K), 0.0008654 secs]
null
null
null
[GC (Allocation Failure)  1655K->973K(3584K), 0.0008111 secs]
null
...省略幾個null的輸出
[GC (Allocation Failure)  1980K->1997K(3584K), 0.0009289 secs]
[Full GC (Ergonomics)  1997K->1870K(3584K), 0.0048483 secs]
--- 弱引用物件被jvm回收了 ---- 
[email protected]
--- 回收物件 ---- null null ...省略幾個null和幾次Full GC的輸出 [Full GC (Ergonomics) 2971K->2971K(3584K), 0.0024850 secs] [Full GC (Allocation Failure) 2971K->2971K(3584K), 0.0022460 secs] Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space at weakhashmap.PhantomReferenceTest.lambda$main$0(PhantomReferenceTest.java:20) at weakhashmap.PhantomReferenceTest$$Lambda$1/2065951873.run(Unknown Source) at java.lang.Thread.run(Thread.java:748)

因為設定的虛擬機器堆大小比較小,所以建立一個100k的物件時直接進入了老年代,等到發生Full GC時才會被掃描然後回收。

適用場景

使用虛引用的目的就是為了得知物件被GC的時機,所以可以利用虛引用來進行銷燬前的一些操作,比如說資源釋放等。這個虛引用對於物件而言完全是無感知的,有沒有完全一樣,但是對於虛引用的使用者而言,就像是待觀察的物件的把脈線,可以通過它來觀察物件是否已經被回收,從而進行相應的處理。

事實上,虛引用有一個很重要的用途就是用來做堆外記憶體的釋放,DirectByteBuffer就是通過虛引用來實現堆外記憶體的釋放的。

小結

  • 虛引用是最弱的引用
  • 虛引用對物件而言是無感知的,物件有虛引用跟沒有是完全一樣的
  • 虛引用不會影響物件的生命週期
  • 虛引用可以用來做為物件是否存活的監控