前言
爲了滿足對不同情況的垃圾回收需求,Java從版本1.2開始,引入了4種引用類型(其實是額外增加了三種)的概念。本文將詳細介紹這四種引用。
Java 4種引用類型
Java中的4中引用類型分別爲強引用(String Reference),軟引用(Soft Reference),弱引用(Weak Reference)和虛引用(Phantom Reference)。
概念及應用場景
- 強引用:Java中的引用,默認都是強引用。比如new一個對象,對它的引用就是強引用。對於被強引用指向的對象,就算JVM內存不足OOM,也不會去回收它們。
- 軟引用:若一個對象只被軟引用所引用,那麼它將在JVM內存不足的時候被回收,即如果JVM內存足夠,則軟引用所指向的對象不會被垃圾回收(其實這個說法也不夠準確,具體原因後面再說)。根據這個性質,軟引用很適合做內存緩存:既能提高查詢效率,也不會造成內存泄漏。
- 弱引用:若一個對象只被弱引用所引用,那麼它將在下一次GC中被回收掉。如ThreadLocal和WeakHashMap中都使用了弱引用,防止內存泄漏。
- 虛引用:虛引用是四種引用中最弱的一種引用。我們永遠無法從虛引用中拿到對象,被虛引用引用的對象就跟不存在一樣。虛引用一般用來跟蹤垃圾回收情況,或者可以完成垃圾收集器之外的一些定製化操作。Java NIO中的堆外內存(DirectByteBuffer)因爲不受GC的管理,這些內存的清理就是通過虛引用來完成的。
引用隊列
引用隊列(Reference Queue)是一個鏈表,顧名思義,存放的是引用對象(Reference對象)的隊列。
軟引用與弱引用可以和一個引用隊列配合使用,當引用所指向的對象被垃圾回收之後,該引用對象本身會被添加到與之關聯的引用隊列中,從而方便後續一些跟蹤或者額外的清理操作。
因爲無法從虛引用中拿到目標對象,虛引用必須和一個引用隊列配合使用。
案例解析
設置JVM的啓動參數爲
-Xms10m -Xmx10m
public class ReferenceTest {
private static int _1MB = 1024 * 1024;
private static int _1KB = 1024;
public static void main(String[] args) throws InterruptedException {
// 引用隊列,存放Reference對象
ReferenceQueue queue = new ReferenceQueue();
// 定義四種引用對象,強/弱/虛引用爲1kb,軟引用爲1mb
Byte[] strong = new Byte[_1KB];
SoftReference<Byte[]> soft = new SoftReference<>(new Byte[_1MB], queue);
WeakReference<Byte[]> weak = new WeakReference<>(new Byte[_1KB], queue);
PhantomReference<Byte[]> phantom = new PhantomReference<>(new Byte[_1KB], queue);
Reference<String> collectedReference;
// 初始狀態
System.out.println("Init: Strong Reference is " + strong);
System.out.println("Init: Soft Reference is " + soft.get());
System.out.println("Init: Weak Reference is " + weak.get());
System.out.println("Init: Phantom Reference is " + phantom.get());
do {
collectedReference = queue.poll();
System.out.println("Init: Reference In Queue is " + collectedReference);
}
while (collectedReference != null);
System.out.println("********************");
// 第一次手動觸發GC
System.gc();
// 停100ms保證垃圾回收已經執行
Thread.sleep(100);
System.out.println("After GC: Strong Reference is " + strong);
System.out.println("After GC: Soft Reference is " + soft.get());
System.out.println("After GC: Weak Reference is " + weak.get());
System.out.println("After GC: Phantom Reference is " + phantom.get());
do {
collectedReference = queue.poll();
System.out.println("After GC: Reference In Queue is " + collectedReference);
}
while (collectedReference != null);
System.out.println("********************");
// 再分配1M的內存,以模擬OOM的情況
Byte[] newByte = new Byte[_1MB];
System.out.println("After OOM: Strong Reference is " + strong);
System.out.println("After OOM: Soft Reference is " + soft.get());
System.out.println("After OOM: Weak Reference is " + weak.get());
System.out.println("After OOM: Phantom Reference is " + phantom.get());
do {
collectedReference = queue.poll();
System.out.println("After OOM: Reference In Queue is " + collectedReference);
}
while (collectedReference != null);
}
}
上述代碼的輸出結果爲:
Init: Strong Reference is [Ljava.lang.Byte;@74a14482
Init: Soft Reference is [Ljava.lang.Byte;@1540e19d
Init: Weak Reference is [Ljava.lang.Byte;@677327b6
Init: Phantom Reference is null
Init: Reference In Queue is null
********************
After GC: Strong Reference is [Ljava.lang.Byte;@74a14482
After GC: Soft Reference is [Ljava.lang.Byte;@1540e19d
After GC: Weak Reference is null
After GC: Phantom Reference is null
After GC: Reference In Queue is java.lang.ref.WeakReference@14ae5a5
After GC: Reference In Queue is java.lang.ref.PhantomReference@7f31245a
After GC: Reference In Queue is null
********************
After OOM: Strong Reference is [Ljava.lang.Byte;@74a14482
After OOM: Soft Reference is null
After OOM: Weak Reference is null
After OOM: Phantom Reference is null
After OOM: Reference In Queue is java.lang.ref.SoftReference@6d6f6e28
After OOM: Reference In Queue is null
- 初始狀態下,虛引用用就返回null,其他三個引用都有值。
- 當觸發GC之後,弱引用指向的對象也被回收了,而且可以看到弱引用和虛引用兩個引用對象被加到了它們相關聯的引用隊列中了;強引用和軟引用還是可以取到值。
- 當JVM內存不足之後,軟引用也被內存回收了,同時該軟引用也被加到了與之關聯的引用隊列中了。而強引用依然能取到值。
源碼解析
以下是引用類的UML圖
弱引用,軟引用和虛引用都繼承自Reference類,我們從Reference類看起
Reference類
// 此Reference對象可能會有四種狀態:active, pending, enqueued, inactive
// avtive: 新創建的對象狀態是active
// pending: 當Reference所指向的對象不可達,並且Reference與一個引用隊列關聯,那麼垃圾收集器
// 會將Reference標記爲pending,並且會將之加到pending隊列裏面
// enqueued: 當Reference從pending隊列中,移到引用隊列中之後,就是enqueued狀態
// inactive: 如果Reference所指向的對象不可達,並且Reference沒有與引用隊列關聯,Reference
// 從引用隊列移除之後,變爲inactive狀態。inactive就是最終狀態
public abstract class Reference<T> {
// 該對象就是Reference所指向的對象,垃圾收集器會對此對象做特殊處理。
private T referent; /* Treated specially by GC */
// Reference相關聯的引用隊列
volatile ReferenceQueue<? super T> queue;
// 當Reference是active時,next爲null
// 當該Reference處於引用隊列中時,next指向隊列中的下一個Reference
// 其他情況next指向this,即自己
// 垃圾收集器只需判斷next是不是爲null,來看是否需要對此Reference做特殊處理
volatile Reference next;
// 當Reference在pending隊列中時,該值指向下一個隊列中Reference對象
// 另外垃圾收集器在GC過程中,也會用此對象做標記
transient private Reference<T> discovered; /* used by VM */
// 鎖對象
static private class Lock { }
private static Lock lock = new Lock();
// pending隊列,這裏的pending是pending鏈表的隊首元素,一般與上面的discovered變量一起使用
private static Reference<Object> pending = null;
// 獲取Reference指向的對象。默認返回referent對象
public T get() {
return this.referent;
}
}
Reference類跟垃圾收集器緊密關聯,其狀態變化如下圖所示:
上述步驟大多數都是由GC線程來完成,其中Pending到Enqueued是用戶線程來做的。Reference類中定義了一個子類ReferenceHandler,專門用來處理Pending狀態的Reference。我們來看看它具體做了什麼。
ReferenceHandler類
public abstract class Reference<T> {
// 靜態塊,主要邏輯是啓動ReferenceHandler線程
static {
// 創建ReferenceHandler線程
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
// 設置成守護線程,最高優先級,並啓動
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// 訪問控制
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
// 內部類ReferenceHandler,用來處理Pending狀態的Reference
private static class ReferenceHandler extends Thread {
private static void ensureClassInitialized(Class<?> clazz) {
try {
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException e) {
throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
}
}
// 靜態塊,確保InterruptedException和Cleaner已經被ClassLoader加載
// 因爲後面會用到這兩個類
static {
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
// 死循環調用tryHandlePending方法
while (true) {
tryHandlePending(true);
}
}
}
}
Reference類在加載進JVM的時候,會啓動ReferenceHandler線程,並將它設成最高優先級的守護線程,不斷循環調用tryHandlePending方法。
接下來看tryHandlePending方法:
// waitForNotify默認是true。
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
// 需要在同步塊中進行
synchronized (lock) {
// 判斷pending隊列是否爲空,pending是隊首元素
if (pending != null) {
// 取到pending隊列隊首元素,賦值給r
r = pending;
// Cleaner類是Java NIO中專門用來清理堆外內存(DirectByteBufer)的類,這裏對它做了特殊處理
// 當沒有其他引用指向堆外內存時,與之關聯的Cleaner會被加到pending隊列中
// 如果該Reference是Cleaner實例,那麼取到該Cleaner,後續可以做一些清理操作。
c = r instanceof Cleaner ? (Cleaner) r : null;
// r.discovered就是下一個元素
// 以下操作即爲將隊首元素從pending隊列移除
pending = r.discovered;
r.discovered = null;
} else {
// 如果pending隊列爲空,則釋放鎖等待
// 當有Reference添加到pending隊列中時,ReferenceHandler線程會從此處被喚醒
if (waitForNotify) {
lock.wait();
}
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// OOM時,讓出cpu
Thread.yield();
return true;
} catch (InterruptedException x) {
return true;
}
// 給Cleaner的特殊處理,調用clean()方法,以釋放與之關聯的堆外內存
if (c != null) {
c.clean();
return true;
}
// 此處,將此Reference加入到與之關聯的引用隊列
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
看到這裏,豁然開朗。ReferenceHandler線程專門用來處理pending狀態的Reference,跟GC線程組成類似生產者消費者的關係。當pending隊列爲空,則等待;當Reference關聯的對象被回收,Reference被加入到pending隊列中之後,ReferenceHandler線程會被喚醒來處理pending的Reference,主要做三件事:
- 將該Reference從pending隊列移除
- 如果該Reference是Cleaner的實例,那麼調用clean方法,釋放堆外內存
- 將Reference加入到與之關聯的引用隊列
ReferenceQueue
引用隊列比較簡單,可以直接理解爲一個存放Reference的鏈表,在此不再費筆墨。
虛引用PhantomReference
// 灰常簡單,只重寫了一個構造方法,一個get方法
public class PhantomReference<T> extends Reference<T> {
// get方法永遠返回null
public T get() {
return null;
}
// 只提供了一個包含ReferenceQueue的構造方法,說明它必須和引用隊列一起使用
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
一般情況下虛引用使用得比較少,最爲人所熟知的就是PhantomReference的子類Cleaner了,它用來清理NIO中的堆外內存。有機會可以專門寫篇文章來講講它。
弱引用WeakReference
// 更加簡單,只重寫了兩個構造方法
public class WeakReference<T> extends Reference<T> {
public WeakReference(T referent) {
super(referent);
}
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
太過簡單,不做額外講解。
軟引用SoftReference
// 相比WeakReference,它增加了兩個時間戳,clock和timestamp
// 這兩個參數是實現他們內存回收上區別的關鍵
public class SoftReference<T> extends Reference<T> {
// 每次GC之後,若該引用指向的對象沒有被回收,則垃圾收集器會將clock更新成當前時間
static private long clock;
// 每次調用get方法的時候,會更新該時間戳爲clock值
// 所以該值保存的是上一次(最近一次)GC的時間戳
private long timestamp;
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}
public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.timestamp = clock;
}
// 每次調用,更新timestamp的值,使之等於clock的值,即最近一次gc的時間
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}
SoftReference除了多了兩個時間戳之外,跟WeakReference幾乎沒有區別,它是如何做到在內存不足時被回收這件事的呢?其實這是垃圾收集器乾的活。垃圾收集器回收SoftReference所指向的對象,會看兩個維度:
- SoftReference.timestamp有多老(距上一次GC過了多久)
- JVM的堆空閒空間有多大
而具體什麼時候回收SoftReference所指向的對象呢,可以參考如下公式:
interval <= free_heap * ms_per_mb
其中interval爲上一次GC與當前時間的差值,以毫秒爲單位;free_heap爲當前JVM中剩餘的堆空間大小,以MB爲單位;ms_per_mb可以理解爲一個常數,即每兆空閒空間可維持的SoftReference的對象生存的時長,默認爲1000,可以通過JVM參數*-XX:SoftRefLRUPolicyMSPerMB設置。
如果上述表達式返回false,則清理SoftReference所指向的對象,並將該SoftReference加入到pending*隊列中;否則不做處理。所以說在JVM內存不足的時候回收軟引用這個說法不是非常準確,只是個經驗說法,軟引用的回收,還跟它存活的時間有關,甚至跟JVM參數設置(-XX:SoftRefLRUPolicyMSPerMB)都有關係!