前言
為了滿足對不同情況的垃圾回收需求,從Java從版本1.2開始,引入了4種引用類型(其實是額外增加了三種)的概念。本文將詳細介紹這四種引用。
Java 4種引用類型
Java中的4中引用類型分別為強引用(String Reference),軟引用(Soft Reference),弱引用(Weak Reference)和虛引用(Phantom Reference)。
概念及應(yīng)用場景
- 強引用:Java中的引用,默認都是強引用。比如new一個對象,對它的引用就是強引用。對于被強引用指向的對象,就算JVM內(nèi)存不足OOM,也不會去回收它們。
- 軟引用:若一個對象只被軟引用所引用,那么它將在JVM內(nèi)存不足的時候被回收,即如果JVM內(nèi)存足夠,則軟引用所指向的對象不會被垃圾回收(其實這個說法也不夠準確,具體原因后面再說)。根據(jù)這個性質(zhì),軟引用很適合做內(nèi)存緩存:既能提高查詢效率,也不會造成內(nèi)存泄漏。
- 弱引用:若一個對象只被弱引用所引用,那么它將在下一次GC中被回收掉。如ThreadLocal和WeakHashMap中都使用了弱引用,防止內(nèi)存泄漏。
- 虛引用:虛引用是四種引用中最弱的一種引用。我們永遠無法從虛引用中拿到對象,被虛引用引用的對象就跟不存在一樣。虛引用一般用來跟蹤垃圾回收情況,或者可以完成垃圾收集器之外的一些定制化操作。Java NIO中的堆外內(nèi)存(DirectByteBuffer)因為不受GC的管理,這些內(nèi)存的清理就是通過虛引用來完成的。
引用隊列
引用隊列(Reference Queue)是一個鏈表,顧名思義,存放的是引用對象(Reference對象)的隊列。
軟引用與弱引用可以和一個引用隊列(Reference Queue)配合使用,當引用所指向的對象被垃圾回收之后,該引用對象本身會被添加到與之關(guān)聯(lián)的引用隊列中,從而方便后續(xù)一些跟蹤或者額外的清理操作。
因為無法從虛引用中拿到目標對象,虛引用必須和一個引用隊列(Reference Queue)配合使用。
案例解析
設(shè)置JVM的啟動參數(shù)為
-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;
// 初始狀態(tài)
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("********************");
// 第一次手動觸發(fā)GC
System.gc();
// 停100ms保證垃圾回收已經(jīng)執(zhí)行
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的內(nèi)存,以模擬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);
}
}
上述代碼的輸出結(jié)果為:
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
- 初始狀態(tài)下,虛引用用就返回null,其他三個引用都有值。
- 當觸發(fā)GC之后,弱引用指向的對象也被回收了,而且可以看到弱引用和虛引用兩個引用對象被加到了它們相關(guān)聯(lián)的引用隊列中了;強引用和軟引用還是可以取到值。
- 當JVM內(nèi)存不足之后,軟引用也被內(nèi)存回收了,同時該軟引用也被加到了與之關(guān)聯(lián)的引用隊列中了。而強引用依然能取到值。
源碼解析
以下是引用類的UML圖

弱引用,軟引用和虛引用都繼承自Reference類,我們從Reference類看起
Reference類
// 此Reference對象可能會有四種狀態(tài):active, pending, enqueued, inactive
// avtive: 新創(chuàng)建的對象狀態(tài)是active
// pending: 當Reference所指向的對象不可達,并且Reference與一個引用隊列關(guān)聯(lián),那么垃圾收集器
// 會將Reference標記為pending,并且會將之加到pending隊列里面
// enqueued: 當Reference從pending隊列中,移到引用隊列中之后,就是enqueued狀態(tài)
// inactive: 如果Reference所指向的對象不可達,并且Reference沒有與引用隊列關(guān)聯(lián),Reference
// 從引用隊列移除之后,變?yōu)閕nactive狀態(tài)。inactive就是最終狀態(tài)
public abstract class Reference<T> {
// 該對象就是Reference所指向的對象,垃圾收集器會對此對象做特殊處理。
private T referent; /* Treated specially by GC */
// Reference相關(guān)聯(lián)的引用隊列
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類跟垃圾收集器緊密關(guān)聯(lián),其狀態(tài)變化如下圖所示:

上述步驟大多數(shù)都是由GC線程來完成,其中Pending到Enqueued是用戶線程來做的。Reference類中定義了一個子類ReferenceHandler,專門用來處理Pending狀態(tài)的Reference。我們來看看它具體做了什么。
ReferenceHandler類
public abstract class Reference<T> {
// 靜態(tài)塊,主要邏輯是啟動ReferenceHandler線程
static {
// 創(chuàng)建ReferenceHandler線程
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
// 設(shè)置成守護線程,最高優(yōu)先級,并啟動
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// 訪問控制
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
// 內(nèi)部類ReferenceHandler,用來處理Pending狀態(tài)的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);
}
}
// 靜態(tài)塊,確保InterruptedException和Cleaner已經(jīng)被ClassLoader加載
// 因為后面會用到這兩個類
static {
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
// 死循環(huán)調(diào)用tryHandlePending方法
while (true) {
tryHandlePending(true);
}
}
}
}
Reference類在加載進JVM的時候,會啟動ReferenceHandler線程,并將它設(shè)成最高優(yōu)先級的守護線程,不斷循環(huán)調(diào)用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中專門用來清理堆外內(nèi)存(DirectByteBufer)的類,這里對它做了特殊處理
// 當沒有其他引用指向堆外內(nèi)存時,與之關(guān)聯(lián)的Cleaner會被加到pending隊列中
// 如果該Reference是Cleaner實例,那么取到該Cleaner,后續(xù)可以做一些清理操作。
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的特殊處理,調(diào)用clean()方法,以釋放與之關(guān)聯(lián)的堆外內(nèi)存
if (c != null) {
c.clean();
return true;
}
// 此處,將此Reference加入到與之關(guān)聯(lián)的引用隊列
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
看到這里,豁然開朗。ReferenceHandler線程專門用來處理pending狀態(tài)的Reference,跟GC線程組成類似生產(chǎn)者消費者的關(guān)系。當pending隊列為空,則等待;當Reference關(guān)聯(lián)的對象被回收,Reference被加入到pending隊列中之后,ReferenceHandler線程會被喚醒來處理pending的Reference,主要做三件事:
- 將該Reference從pending隊列移除
- 如果該Reference是Cleaner的實例,那么調(diào)用clean方法,釋放堆外內(nèi)存
- 將Reference加入到與之關(guān)聯(lián)的引用隊列
ReferenceQueue
引用隊列比較簡單,可以直接理解為一個存放Reference的鏈表,在此不再費筆墨。
虛引用PhantomReference
// 灰常簡單,只重寫了一個構(gòu)造方法,一個get方法
public class PhantomReference<T> extends Reference<T> {
// get方法永遠返回null
public T get() {
return null;
}
// 只提供了一個包含ReferenceQueue的構(gòu)造方法,說明它必須和引用隊列一起使用
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
一般情況下虛引用使用得比較少,最為人所熟知的就是PhantomReference的子類Cleaner了,它用來清理NIO中的堆外內(nèi)存。有機會可以專門寫篇文章來講講它。
弱引用WeakReference
// 更加簡單,只重寫了兩個構(gòu)造方法
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
// 這兩個參數(shù)是實現(xiàn)他們內(nèi)存回收上區(qū)別的關(guān)鍵
public class SoftReference<T> extends Reference<T> {
// 每次GC之后,若該引用指向的對象沒有被回收,則垃圾收集器會將clock更新成當前時間
static private long clock;
// 每次調(diào)用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;
}
// 每次調(diào)用,更新timestamp的值,使之等于clock的值,即最近一次gc的時間
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}
SoftReference除了多了兩個時間戳之外,跟WeakReference幾乎沒有區(qū)別,它是如何做到在內(nèi)存不足時被回收這件事的呢?其實這是垃圾收集器干的活。垃圾收集器回收SoftReference所指向的對象,會看兩個維度:
- SoftReference.timestamp有多老(距上一次GC過了多久)
- JVM的堆空閑空間有多大
而具體什么時候回收SoftReference所指向的對象呢,可以參考如下公式:
interval <= free_heap * ms_per_mb
其中interval為上一次GC與當前時間的差值,以毫秒為單位;free_heap為當前JVM中剩余的堆空間大小,以MB為單位;ms_per_mb可以理解為一個常數(shù),即每兆空閑空間可維持的SoftReference的對象生存的時長,默認為1000,可以通過JVM參數(shù)-XX:SoftRefLRUPolicyMSPerMB設(shè)置。
如果上述表達式返回false,則清理SoftReference所指向的對象,并將該SoftReference加入到pending隊列中;否則不做處理。所以說在JVM內(nèi)存不足的時候回收軟引用這個說法不是非常準確,只是個經(jīng)驗說法,軟引用的回收,還跟它存活的時間有關(guān),甚至跟JVM參數(shù)設(shè)置(-XX:SoftRefLRUPolicyMSPerMB)都有關(guān)系!