一次性把Java的四種引用說清楚!

語言: CN / TW / HK

前幾天在CodeReview的時候,看到了一個用WeakHashMap的程式碼,進而聊到了WeakReference,再聊到Java四種引用型別。

回想了一下,上次學習Java的強軟弱虛四種引用型別,還是在準備面試的時候。平時用得不多,一下子竟然想不清楚它們的區別,只記得它們的強度依次遞減。

下來又看了一下這方面的文章,今天好好把它們理清楚。

四種引用的區別

其實四種引用的區別在於GC的時候,對它們的處理不同。用一句話來概括,就是:如果一個物件GC Root可達,強引用不會被回收,軟引用在記憶體不足時會被回收,弱引用在這個物件第一次GC會被回收。

如果GC Root不可達,那不論什麼引用,都會被回收

虛引用比較特殊,等於沒有引用,不會影響物件的生命週期,但可以在物件被收集器回收時收到一個系統通知。

下面結合案例分別來講一下四種引用在面對GC時的表現以及它們的常見用途。先設定一下JVM的引數:

-Xms20M -Xmx20M -Xmn10M -verbose:gc -XX:+PrintGCDetails
複製程式碼

強引用

這就是我們平時最常使用的引用。只要GC的時候這個物件GC Root可達,它就不會被回收。如果JVM記憶體不夠了,直接丟擲OOM。比如下面這段程式碼就會丟擲OutOfMemoryError

public static void main(String[] args) {
    List<Object> list = new LinkedList<>();
    for (int i = 0; i < 21; i++) {
        list.add(new byte[1024 * 1024]);
    }
}
複製程式碼

軟引用

軟引用,當GC的時候,如果GC Root可達,如果記憶體足夠,就不會被回收;如果記憶體不夠用,會被回收。將上面的例子改成軟引用,就不會被OOM:

public static void main(String[] args) {
    List<Object> list = new LinkedList<>();
    for (int i = 0; i < 21; i++) {
        SoftReference<byte[]> softReference = new SoftReference<>(new byte[1024 * 1024]);
        list.add(softReference);
    }
}
複製程式碼

我們把程式改造一下,打印出GC後的前後的差別:

public static void main(String[] args) {
    List<SoftReference<byte[]>> list = new LinkedList<>();
    for (int i = 0; i < 21; i++) {
        SoftReference<byte[]> softReference = new SoftReference<>(new byte[1024 * 1024]);
        list.add(softReference);
        System.out.println("gc前:" + softReference.get());
    }
    System.gc();
    for (SoftReference<byte[]> softReference : list) {
        System.out.println("gc後:" + softReference.get());
    }
}
複製程式碼

會發現,打印出的日誌,GC前都是有值的,而GC後,會有一些是null,代表它們已經被回收。

而我們設定的堆最大為20M,如果把迴圈次數改成15,就會發現打印出的日誌,GC後沒有為null的。但通過-verbose:gc -XX:+PrintGCDetails引數能發現,JVM還是進行了幾次GC的,只是由於記憶體還夠用,所以沒有回收。

public static void main(String[] args) {
    List<SoftReference<byte[]>> list = new LinkedList<>();
    for (int i = 0; i < 15; i++) {
        SoftReference<byte[]> softReference = new SoftReference<>(new byte[1024 * 1024]);
        list.add(softReference);
        System.out.println("gc前:" + softReference.get());
    }
    System.gc();
    for (SoftReference<byte[]> softReference : list) {
        System.out.println("gc後:" + softReference.get());
    }
}
複製程式碼

所以軟引用的常見用途就呼之欲出了:快取。尤其是那種希望這個快取能夠持續時間長一點的。

弱引用

軟引用,只要這個物件發生GC,就會被回收。

把上面的程式碼改成軟引用,會發現打印出的日誌,GC後全部為null

public static void main(String[] args) {
    List<WeakReference<byte[]>> list = new LinkedList<>();
    for (int i = 0; i < 15; i++) {
        WeakReference<byte[]> weakReference = new WeakReference<>(new byte[1024 * 1024]);
        list.add(weakReference);
        System.out.println("gc前:" + weakReference.get());
    }
    System.gc();
    for (WeakReference<byte[]> weakReference : list) {
        System.out.println("gc後:" + weakReference.get());
    }
}
複製程式碼

所以弱引用也適合用來做快取,不過由於它是隻要發生GC就會被回收,所以存活的時間比軟引用短得多,通常用於做一些非常臨時的快取。

我們知道,WeakHashMap內部是通過弱引用來管理entry的。它的鍵是“弱鍵”,所以在GC時,它對應的鍵值對也會從Map中刪除。

Tomcat中有一個ConcurrentCache,用到了WeakHashMap,結合ConcurrentHashMap,實現了一個執行緒安全的快取,感興趣的同學可以研究一下原始碼,程式碼非常精簡,加上所有註釋,只有短短59行。

ThreadLocal中的靜態內部類ThreadLocalMap裡面的entry是一個WeakReference的繼承類。

使用弱引用,使得ThreadLocalMap知道ThreadLocal物件是否已經失效,一旦該物件失效,也就是成為垃圾,那麼它所操控的Map裡的資料也就沒有用處了,因為外界再也無法訪問,進而決定擦除Map中相關的值物件,Entry物件的引用,來保證Map總是保持儘可能的小。

虛引用

虛引用的設計和上面三種引用有些不同,它並不影響GC,而是為了在物件被GC時,能夠收到一個系統通知。

那它是怎麼被通知的呢?虛引用必須要配合ReferenceQueue,當GC準備回收一個物件,如果發現它還有虛引用,就會在回收之前,把這個虛引用加入到與之關聯的ReferenceQueue中。

那NIO是如何利用虛引用來管理記憶體的呢?

DirectBuffer直接從Java堆之外申請一塊記憶體, 這塊記憶體是不直接受JVM GC管理的, 也就是說在GC演算法中並不會直接操作這塊記憶體. 這塊記憶體的GC是由於DirectBuffer在Java堆中的物件被GC後, 通過一個通知機制, 而將其清理掉的.

DirectBuffer內部有一個Cleaner。這個Cleaner是PhantomReference的子類。當DirectBuffer物件被回收之後, 就會通知到PhantomReference。然後由ReferenceHandler呼叫tryHandlePending()方法進行pending處理. 如果pending不為空, 說明DirectBuffer被回收了, 就可以呼叫Cleaner的clean()進行回收了。

上面這個方法的程式碼在Reference類裡面,有興趣的同學可以去看一下那個方法的原始碼。

總結

以上就是Java中四種引用的區別。一般來說,強引用我們都知道,虛引用很少用到。而軟引用和弱引用的區別在於回收的時機:軟引用GC時,發現記憶體不夠才回收,弱引用只要一GC就會回收。

關於作者

微信公眾號:編了個程

個人網站:https://yasinshaw.com

筆名Yasin,一個有深度,有態度,有溫度的程式設計師。工作之餘分享程式設計技術和生活,如果喜歡我的文章,可以順手關注一下公眾號,也歡迎轉發分享給你的朋友~

在公眾號回覆“面試”或者“學習”可以領取相應的資源哦~

公眾號
公眾號