一次性把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就會回收。

關於作者

微信公眾號:編了個程

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

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

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

公眾號
公眾號