前幾天在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,一個有深度,有態度,有温度的程序員。工作之餘分享編程技術和生活,如果喜歡我的文章,可以順手「關注」一下公眾號,也歡迎「轉發」分享給你的朋友~
在公眾號回覆“面試”或者“學習”可以領取相應的資源哦~