1、引用級別
在JVM中,垃圾回收器一直在背后默默地承擔(dān)著內(nèi)存回收的工作,我們不需要像使用C語音開發(fā)那樣小心翼翼地管理內(nèi)存。但是凡事皆有兩面性,這種機制的好處是極大地釋放了程序員無處安放的焦慮,壞處是難以對回收過程進行更靈活地干預(yù)。
為了增加對垃圾回收的力度把控,Java引入了引用級別的概念。
在JDK 1.2以前的版本中,只有在對象沒有任何其他對象引用它時,垃圾回收器才會對它進行收集。對象只有被引用和沒有被引用兩種狀態(tài)。這種方式無法描述一些“食之無味,棄之可惜”的對象。而很多時候,我們希望存在這樣一些對象:當(dāng)內(nèi)存空間足夠時,不進行回收;當(dāng)內(nèi)存空間變得緊張時,允許JVM回收這些對象。大部分緩存都符合這樣的場景。
從JDK 1.2版本開始,Java對引用的概念進行了擴充,對象的引用分成了4種級別,從而使程序開發(fā)者能更加靈活地控制對象的生命周期,更好的控制創(chuàng)建的對象何時被釋放和回收。
這4種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。
1.1 強引用(StrongReference)
我們使用的大部分的引用都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當(dāng)內(nèi)存空間不足,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內(nèi)存不足問題。
如果強引用對象不使用時,需要弱化從而使GC能夠回收,如:
strongReference = null;
顯式地設(shè)置strongReference對象為null,或讓其超出對象的生命周期范圍,則gc認(rèn)為該對象不存在引用,這時就可以回收這個對象。具體什么時候收集這要取決于GC算法。
public void test() {
Object strongReference = new Object();
// 省略其他操作
}
在一個方法的內(nèi)部有一個強引用,這個引用保存在Java棧中,而真正的引用內(nèi)容(Object)保存在Java堆中。 當(dāng)這個方法運行完成后,就會退出方法棧,則引用對象的引用數(shù)為0,這個對象會被回收。但是如果這個strongReference是全局變量時,就需要在不用這個對象時賦值為null,因為強引用不會被垃圾回收。
ArrayList的Clear方法:
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
在ArrayList類中定義了一個elementData數(shù)組,在調(diào)用clear方法清空數(shù)組時,每個數(shù)組元素被賦值為null。 但是,這里為什么不直接將數(shù)組設(shè)置為null:
elementData=null;
一行代碼搞定,豈不更加簡單粗暴?
如果將整個數(shù)組指向null,該數(shù)組的內(nèi)存空間會被回收掉,而將數(shù)組的元素逐個指向null只會把數(shù)組中存放的強引用釋放,整個數(shù)組對象還是存在的,這樣以來,如果再有add()等操作就不用再次分配內(nèi)存了。
1.2 軟引用(SoftReference)
如果一個對象只具有軟引用,那就類似于可有可無的生活用品。如果內(nèi)存空間足夠,垃圾回收器就不會回收它,如果內(nèi)存空間不足了,就會回收這些對象的內(nèi)存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現(xiàn)內(nèi)存敏感的高速緩存。
軟引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被垃圾回收,JAVA虛擬機就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中。
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);
str = null;
// Notify GC
System.gc();
System.out.println(softReference.get()); // abc
Reference<? extends String> reference = referenceQueue.poll();
System.out.println(reference); //null
注意: 軟引用對象是在jvm內(nèi)存不夠的時候才會被回收,我們調(diào)用System.gc()方法只是起通知作用,JVM什么時候掃描回收對象是JVM自己的狀態(tài)決定的。就算掃描到軟引用對象也不一定會回收它,只有內(nèi)存不夠的時候才會回收。
當(dāng)內(nèi)存不足時,JVM首先將軟引用中的對象引用置為null,然后通知垃圾回收器進行回收。也就是說,垃圾收集線程會在虛擬機拋出OutOfMemoryError之前回收軟引用對象,而且虛擬機會盡可能優(yōu)先回收長時間閑置不用的軟引用對象。對那些剛構(gòu)建的或剛使用過的“較新的”軟對象會被虛擬機盡可能保留,這就是引入引用隊列ReferenceQueue的原因。
1.3 弱引用(WeakReference)
弱引用與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它 所管轄的內(nèi)存區(qū)域的過程中,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當(dāng)前內(nèi)存空間足夠與否,都會回收它的內(nèi)存。不過,由于垃圾回收器是一個優(yōu)先級很低的線程, 因此不一定會很快發(fā)現(xiàn)那些只具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關(guān)聯(lián)的引用隊列中。
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
System.out.println(weakReference.get() == null); //false
str = null;
System.gc(); //觸發(fā)垃圾回收
System.out.println(weakReference.get() == null); //true
下面的代碼會讓一個弱引用再次變?yōu)橐粋€強引用:
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
// 弱引用轉(zhuǎn)強引用
String strongReference = weakReference.get();
1.4 虛引用(PhantomReference)
"虛引用"顧名思義,就是形同虛設(shè),與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 創(chuàng)建虛引用,要求必須與一個引用隊列關(guān)聯(lián)
PhantomReference pr = new PhantomReference(str, queue);
虛引用主要用來跟蹤對象被垃圾回收的活動。虛引用與軟引用和弱引用的一個區(qū)別在于:虛引用必須和引用隊列(ReferenceQueue)聯(lián)合使用。當(dāng)垃 圾回收器準(zhǔn)備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前,把這個虛引用加入到與之關(guān)聯(lián)的引用隊列中。程序可以通過判斷引用隊列中是 否已經(jīng)加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發(fā)現(xiàn)某個虛引用已經(jīng)被加入到引用隊列,那么就可以在所引用的對象的內(nèi)存被回收之前采取必要的行動。
2、java.lang.ref包
java.lang.ref包下都是與reference相關(guān)的類,包括:

- Reference:引用對象的抽象基類,定義了所有引用對象的通用操作
- ReferenceQueue: 引用隊列,垃圾回收器在檢測到對象可以回收時,會將該對象的Reference放到隊列(如果有)
- SoftReference:Reference子類,代表軟引用
- WeakReference:Reference子類,代表弱引用
- PhantomReference:Reference子類,代表虛引用
- FinalReference:Reference子類,訪問權(quán)限是package,外部無法訪問,由JVM來實例化,JVM會對那些實現(xiàn)了Object中finalize()方法的類實例化一個對應(yīng)的FinalReference
- Finalizerd:FinalReference子類,訪問權(quán)限是package,且為final類,外部無法訪問。JVM實際操作的是Finalizer。當(dāng)一個類滿足實例化FinalReference的條件時,JVM會調(diào)用Finalizer.register()進行注冊
3、Reference
3.1 內(nèi)部變量
先來看一下Reference類中的變量
private T referent;
volatile ReferenceQueue<? super T> queue;
Reference next;
private static Reference<Object> pending = null;
transient private Reference<T> discovered;
static private class Lock { }
private static Lock lock = new Lock();
- referent
表示其引用的對象,即在構(gòu)造的時候需要被包裝在其中的對象。
- queue
是對象即將被回收時所要通知的隊列。當(dāng)對象即將被回收時,整個reference對象,而不僅僅是被回收的對象,會被放到queue 里面,然后外部程序通過監(jiān)控這個 queue 即可拿到相應(yīng)的數(shù)據(jù)了。ReferenceQueue并不是一個鏈表數(shù)據(jù)結(jié)構(gòu),它只持有這個鏈表的表頭對象header,這個鏈表是由Refence對象里面的next變量構(gòu)建起來,next也就是鏈表當(dāng)前節(jié)點的下一個節(jié)點,所以Reference對象本身就是一個鏈表的節(jié)點,這個鏈表由ReferenceHander線程從pending隊列中取出的數(shù)據(jù)構(gòu)建而來
- next
與queue 搭配使用,僅在Reference放到queue中時才有意義,一系列的Reference正是依靠next組成了一個單鏈表
- pending
pending與后面的discovered一起構(gòu)成了一個pending單向鏈表。注意這個變量是一個靜態(tài)對象,所以是全局唯一的。pending為鏈表的頭節(jié)點,discovered為鏈表當(dāng)前Reference節(jié)點指向下一個節(jié)點的引用,這個隊列是由JVM構(gòu)建的,當(dāng)對象除了被reference引用之外沒有其它強引用了,JVM就會將指向需要回收的對象的Reference都放入到這個隊列里面(注意是指向要回收的對象的Reference,要回收的對象就是Reference的成員變量refernt持有的對象,是refernt持有的對象要被回收,而不是Reference對象本身),這個隊列會由ReferenceHander線程來處理。
- discovered
表示ReferenceHander要處理的pending對象的下一個對象。ReferenceHander只需要不停地拿到pending,然后再通過discovered 不斷地拿到下一個對象賦值給pending即可,直到取到了最有一個。
- lock
lock是pending隊列的全局鎖,只在ReferenceHander線程的run方法里面用到。但是,除了當(dāng)前線程,JVM垃圾回收器線程也會操作pending隊列,所以需要通過這個鎖來防止并發(fā)問題。
3.2 ReferenceHander
ReferenceHanderReference的一個內(nèi)部類,繼承自Thread:
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);
}
}
static {
// 確保依賴的其他類已被加載
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
while (true) {
//啟動后無限循環(huán),調(diào)用tryHandlePending方法
tryHandlePending(true);
}
}
}
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
//此處需要加全局鎖,因為除了當(dāng)前線程,gc線程也會操作pending隊列
synchronized (lock) {
//如果pending隊列不為空,則將第一個Reference對象取出
if (pending != null) {
r = pending;
c = r instanceof Cleaner ? (Cleaner) r : null;
//將頭節(jié)點指向discovered,即隊列中的下一個節(jié)點,這樣就把第一個頭結(jié)點出隊了
pending = r.discovered;
//將當(dāng)前節(jié)點的discovered設(shè)置為null;當(dāng)前節(jié)點已出隊,不再需要鏈表序列
r.discovered = null;
} else {
//如果pending隊列為空且需要等待,則進入阻塞狀態(tài)
if (waitForNotify) {
lock.wait();
}
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
Thread.yield();
return true;
} catch (InterruptedException x) {
return true;
}
// 如果從pending隊列出隊的是一個Cleaner對象,那么直接執(zhí)行其clean()方法
if (c != null) {
c.clean();
//注意這里,這里已經(jīng)不往下執(zhí)行了,所以Cleaner對象是不會進入到隊列里面的,給它設(shè)置ReferenceQueue的作用是為了讓它能進入Pending隊列后被ReferenceHander線程處理
return true;
}
//如果綁定了ReferenceQueue隊列,則將對象插入其中
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
可見,ReferenceHander的任務(wù)就是將pending隊列中要被回收的Reference對象移除出來,如果Reference對象在初始化的時候傳入了ReferenceQueue隊列,那么就把從pending隊列里面移除的Reference放到它自己的ReferenceQueue隊列里,如果沒有ReferenceQueue隊列,那么其關(guān)聯(lián)的對象就不會進入到Pending隊列中,會直接被回收掉。
如果把pending與discovered看做指針,ReferenceHander操作pending隊列的流程可以簡化為下圖:

那么問題來了:
1、Reference 源碼中并沒有發(fā)現(xiàn)給 discovered和 pending 賦值的地方,是誰負(fù)責(zé)它們的初始化呢?
2、ReferenceHandler是什么時候由誰啟動的?
問題1,pending隊列由JVM的垃圾回收器維護,負(fù)責(zé)初始化discovered和 pending,并不斷將新的Reference追加到隊列當(dāng)中。
問題2,去源碼中尋找答案,Reference類中有一段靜態(tài)代碼:
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
//擁有最高優(yōu)先級
handler.setPriority(Thread.MAX_PRIORITY);
//設(shè)為守護線程
handler.setDaemon(true);
//啟動線程
handler.start();
// provide access in SharedSecrets
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
ReferenceHandler是一個擁有最高優(yōu)先級的守護線程,在Reference類加載執(zhí)行cinit的時候被初始化并啟動。
3.3 Reference狀態(tài)及其轉(zhuǎn)換
Reference源碼開頭有一段很長的注釋,說明了Reference對象的四種狀態(tài):
- Active
活動狀態(tài),對象存在強引用狀態(tài),還沒有被回收
- Pending
垃圾回收器將沒有強引用的Reference對象放入到pending隊列中,等待ReferenceHander線程處理(前提是這個Reference對象創(chuàng)建的時候傳入了ReferenceQueue,否則的話對象會直接進入Inactive狀態(tài))
- Enqueued
:ReferenceHander線程將pending隊列中的對象取出來放到ReferenceQueue隊列里
- Inactive
處于此狀態(tài)的Reference對象可以被回收,并且其內(nèi)部封裝的對象也可以被回收掉了,有兩個路徑可以進入此狀態(tài):
路徑一:在創(chuàng)建時沒有傳入ReferenceQueue的Reference對象,被Reference封裝的對象在沒有強引用時,指向它的Reference對象會直接進入此狀態(tài);
路徑二:此Reference對象經(jīng)過前面三個狀態(tài)后,已經(jīng)由外部從ReferenceQueue中獲取到,并且已經(jīng)處理掉了。
狀態(tài)的轉(zhuǎn)換示意圖如下:

從代碼層面來說,Reference對象的狀態(tài)只需要通過成員變量next和queue來判斷:
- Active:next=null
- Pending:next = this, queue = ReferenceQueue
- Enqueued:queue =ReferenceQueue.ENQUEUED
- Inactive:next = this, queue = ReferenceQueue.NULL;
可以結(jié)合后面的ReferenceQueue源碼加深理解。
4、 ReferenceQueue
4.1 內(nèi)部變量
private static class Null<S> extends ReferenceQueue<S> {
boolean enqueue(Reference<? extends S> r) {
return false;
}
}
static ReferenceQueue<Object> NULL = new Null<>();
static ReferenceQueue<Object> ENQUEUED = new Null<>();
static private class Lock { };
private Lock lock = new Lock();
//隊列頭
private volatile Reference<? extends T> head = null;
//隊列長度
private long queueLength = 0;
ReferenceQueue定義了一個內(nèi)部類Null<S>,重寫了enqueue入隊方法,永遠(yuǎn)返回false,所以它不會存儲任何數(shù)據(jù),是一個用來做狀態(tài)識別的空隊列。
成員變量NULL和ENQUEUED是內(nèi)部類的兩個實例,用以標(biāo)識Reference的狀態(tài)。當(dāng)Reference對象創(chuàng)建時沒有指定queue或Reference對象已經(jīng)處于inactive狀態(tài)時,其持有的queue會指向NULL;當(dāng)Reference被ReferenceHander線程從pending隊列移到queue里面時,其持有的queue會指向ENQUEUED。
與Reference類似,ReferenceQueue內(nèi)部也定義了一個Lock類,內(nèi)部沒有任何代碼填充,就是一個標(biāo)志類,用來充當(dāng)鎖的載體。但是,Reference中的Reference中的lock變量是靜態(tài)的,也就是被所有實例共享的;而ReferenceQueue中的lock變量是非靜態(tài)的,也就是每個ReferenceQueue實例獨占一份,這是為什么呢?
原因在于,Reference中的lock用于鎖定pending隊列,pending隊列本身就是靜態(tài)變量,全局唯一,lock變量當(dāng)然也得是靜態(tài)的;而ReferenceQueue是與某個具體的Reference綁定,入隊、出隊等操作對于每個ReferenceQueue實例來說都是相互獨立的,只需要鎖定實例相關(guān)的隊列即可,所以lock是實例變量,而非靜態(tài)變量
head變量持有隊列的隊頭,head與Reference中的next變量構(gòu)成了一個單鏈表。Reference對象是從隊頭做出隊入隊操作,所以它是一個后進先出的隊列,其實在數(shù)據(jù)結(jié)構(gòu)上更像更像一個棧。
4.2 入隊操作
boolean enqueue(Reference<? extends T> r) {
//首先要鎖定隊列
synchronized (lock) {
ReferenceQueue<?> queue = r.queue;
//如果Reference沒有綁定隊列,或者已經(jīng)入隊,直接返回
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
//將Reference設(shè)為已入隊狀態(tài)
r.queue = ENQUEUED;
//如果隊列為空,Reference的next指向自己(自己將會成為隊頭);否則,指向隊頭
r.next = (head == null) ? r : head;
//將自己設(shè)為隊頭
head = r;
//增加隊列長度
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
//喚醒出隊操作的等待線程
lock.notifyAll();
return true;
}
}
入隊的操作很簡單,就是不斷地將新對象插入到隊頭,流程示意圖如下:

4.3 出隊操作
有兩種出隊操作:
- poll:非阻塞式,隊列中沒有對象直接返回
- remove:阻塞式,隊列中沒有對象時阻塞線程,直到有新對象進來,可以設(shè)置最長的阻塞時間
//真正執(zhí)行出隊操作的方法,poll與remove都會調(diào)用
private Reference<? extends T> reallyPoll() {
//獲取隊頭
Reference<? extends T> r = head;
if (r != null) {
//將隊頭重置為下一個對象
head = (r.next == r) ?
null :
r.next;
//出隊的reference設(shè)為已出隊狀態(tài)
r.queue = NULL;
//next指向自己
r.next = r;
//減少隊列長度
queueLength--;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(-1);
}
return r;
}
//隊列為空,則直接返回
return null;
}
//非阻塞式出隊
public Reference<? extends T> poll() {
if (head == null)
return null;
synchronized (lock) {
return reallyPoll();
}
}
//阻塞式出隊
public Reference<? extends T> remove(long timeout)
throws IllegalArgumentException, InterruptedException
{
if (timeout < 0) {
throw new IllegalArgumentException("Negative timeout value");
}
synchronized (lock) {
Reference<? extends T> r = reallyPoll();
//出隊成功,返回結(jié)果
if (r != null) return r;
long start = (timeout == 0) ? 0 : System.nanoTime();
for (;;) {
//出隊失敗,阻塞線程一定時間
lock.wait(timeout);
//期間如果被喚醒,會重試出隊操作
r = reallyPoll();
//出隊成功,返回結(jié)果
if (r != null) return r;
//重試仍然失敗,如果未超時,繼續(xù)循環(huán)
if (timeout != 0) {
long end = System.nanoTime();
timeout -= (end - start) / 1000_000;
if (timeout <= 0) return null;
start = end;
}
}
}
}
出隊的流程示意圖如下:

5、 SoftReference、WeakReference和PhantomReference
SoftReference、WeakReference和PhantomReference是reference的三個子類,主要功能都已在父類定義,無需贅言。
值得額外提一句的是SoftReference,相較于其他子類,其特殊的地方在于多了兩個內(nèi)部變量:clock和timestamp。
//由垃圾回收器維護的一個時鐘
static private long clock;
//每次調(diào)用get方法時,會將使用lock更新這個時間戳
private long timestamp;
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
clock是一個靜態(tài)變量,由垃圾回收器負(fù)責(zé)維護。
timestamp是非靜態(tài)變量,初始值等于clock,并且在每次get的時候更新,JVM會參考timestamp來決定是否回收該引用。
6、 FinalReference和Finalizer
6.1 Finalizer機制
Java有垃圾回收器負(fù)責(zé)回收無用的內(nèi)存空間,但JVM只能管理自己的內(nèi)存空間,對于應(yīng)用運行時需要的其它native資源(jvm通過jni暴漏出來的功能):例如直接內(nèi)存DirectByteBuffer,網(wǎng)絡(luò)連接SocksSocketImpl,文件流FileInputStream等與操作系統(tǒng)有交互的資源,JVM就無能為力了,需要我們自己來調(diào)用釋放資源的方法。
但是人在計算機世界中是個最不靠譜的因素,一旦程序員沒有手動釋放這些資源,豈不會導(dǎo)致資源泄露?為了幫助愚蠢的人類,Java提供了finalizer機制:如果對象實現(xiàn)了Object.finalize()方法,JVM會在回收對象之前調(diào)用該方法,釋放掉外部資源。
例如,F(xiàn)ileInputStream的finalize():
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
/* if fd is shared, the references in FileDescriptor
* will ensure that finalizer is only called when
* safe to do so. All references using the fd have
* become unreachable. We can call close()
*/
close();
}
}
SocksSocketImpl則在父類AbstractPlainSocketImpl中實現(xiàn)了finalize()方法:
protected void finalize() throws IOException {
close();
}
6.2 unfinalized隊列
Finalizer機制與FinalReference類、Finalizer類密切相關(guān)。
class FinalReference<T> extends Reference<T> {
public FinalReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
final class Finalizer extends FinalReference<Object> {
private Finalizer(Object finalizee) {
super(finalizee, queue);
add();
}
//...
}
Finalizer繼承FinalReference類,F(xiàn)inalReference繼承Reference類。
FinalReference和Finalizer的訪問權(quán)限是package的,意味著我們不能直接去對其進行擴展。此外,F(xiàn)inalizer類還是final的,構(gòu)造函數(shù)是private的,JDK把它包裝的如此嚴(yán)密,看來不想讓我們過多染指,爾等未開化的草民看看就行了。
先來看下Finalizer類的內(nèi)部變量:
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
private static Finalizer unfinalized = null;
private static final Object lock = new Object();
private Finalizer
next = null,
prev = null;
首先定義了一個ReferenceQueue,會通過Finalizer的構(gòu)造函數(shù)傳給父類,實際上最終充當(dāng)了Reference中的queue變量。值得注意的是,這個queue是是靜態(tài)的,也就是說所有Finalizer共享同一個queue。
此外,還定義了一個慣用的Lock,一個名為unfinalized的變量,兩者也都是靜態(tài)的。
再往下看,又出現(xiàn)了兩個私有變量,一個名為next,一個名為prev,顧名思義,隱約嗅到了一絲雙端鏈表的味道。
//添加節(jié)點到雙端鏈表
private void add() {
//第一步肯定要鎖住鏈表
synchronized (lock) {
//鏈表不為空,將表頭置為自己的后繼節(jié)點
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
//表頭指向自己
unfinalized = this;
}
}
//從雙端鏈表中刪除節(jié)點
private void remove() {
//拿鎖
synchronized (lock) {
//如果表頭是自己,需要先將表頭指向自己的后繼節(jié)點或前驅(qū)節(jié)點
if (unfinalized == this) {
if (this.next != null) {
unfinalized = this.next;
} else {
unfinalized = this.prev;
}
}
//取消自己與后繼節(jié)點的關(guān)聯(lián)
if (this.next != null) {
this.next.prev = this.prev;
}
//取消自己與前驅(qū)節(jié)點的關(guān)聯(lián)
if (this.prev != null) {
this.prev.next = this.next;
}
//后繼節(jié)點與前驅(qū)節(jié)點都指向自己(代表節(jié)點已移除)
this.next = this;
this.prev = this;
}
}
果然如此,F(xiàn)inalizer類實現(xiàn)了雙端鏈表的增加、刪除等方法,unfinalized指向表頭,lock用于鎖定鏈表,next與prev分別指向節(jié)點的后繼與前驅(qū)。
添加示意圖:

刪除示意圖:

6.3 f類
類的修飾有很多,比如final,abstract,public等,如果某個類用final修飾,我們就說這個類是final類,上面列的都是語法層面我們可以顯式指定的,在JVM里其實還會給類標(biāo)記一些其他符號,比如finalizer,表示這個類是一個finalizer類(為了和java.lang.ref.Fianlizer類區(qū)分,finalizer類簡稱為f類),GC在處理這種類的對象時要做一些特殊的處理,如在這個對象被回收之前會調(diào)用它的finalize方法。
JVM在類加載的時候會遍歷當(dāng)前類的所有方法,包括父類的方法,如果有一個方法滿足一下條件,該類就會被標(biāo)記為f類:
- 當(dāng)前類或其父類含有一個參數(shù)為空,返回值為void,名為finalize的方法;
- 這個finalize方法體不能為空;
java.lang.Object里就有一個finalize()方法:
protected void finalize() throws Throwable { }
但是由于其方法體為空,Object并不是一個f類。
6.4 f類的注冊
那么jvm又是在何時調(diào)用register方法的呢?
對象的創(chuàng)建其實是被拆分成多個步驟的,比如A a=new A(2)這樣一條語句對應(yīng)的字節(jié)碼如下:
0: new #1 // class A
3: dup
4: iconst_2
5: invokespecial #11 // Method "<init>":(I)V
先執(zhí)行new分配好對象空間,然后再執(zhí)行invokespecial調(diào)用構(gòu)造函數(shù)。JVM何時調(diào)用Finalizer.register方法,取決于參數(shù)RegisterFinalizersAtInit。改參數(shù)默認(rèn)值為true,代表在構(gòu)造函數(shù)返回之前調(diào)用Finalizer.register方法,如果通過-XX:-RegisterFinalizersAtInit關(guān)閉了該參數(shù),那將在對象空間分配好之后將這個對象注冊進去。
當(dāng)我們通過clone的方式復(fù)制一個對象時,如果當(dāng)前類是一個f類,那么在clone完成時將調(diào)用Finalizer.register方法進行注冊。
6.5 FinalizerThread線程
在Finalizer類的靜態(tài)塊里會創(chuàng)建一個FinalizerThread守護線程,這個線程的優(yōu)先級并不是最高的,意味著在CPU很緊張的情況下其被調(diào)度的優(yōu)先級可能會受到影響。
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
//現(xiàn)成優(yōu)先級設(shè)為8,高于普通現(xiàn)成的優(yōu)先級5
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
//設(shè)為守護線程
finalizer.setDaemon(true);
finalizer.start();
}
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
//如果run方法已經(jīng)在執(zhí)行了,直接退出
if (running)
return;
//等待jvm初始化完成后才繼續(xù)執(zhí)行
while (!VM.isBooted()) {
// delay until VM completes initialization
try {
VM.awaitBooted();
} catch (InterruptedException x) {
// ignore and continue
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
try {
//將對象從ReferenceQueue中移除,
Finalizer f = (Finalizer)queue.remove();
//通過runFinalizer調(diào)用finalizer方法
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
}
private void runFinalizer(JavaLangAccess jla) {
synchronized (this) {
//若next==this,則表明this對象已經(jīng)從unfinalized對象鏈中移除,已經(jīng)執(zhí)行過一次runFinalizer了
if (hasBeenFinalized()) return;
//將該對象從unfinalized隊列中移除
remove();
}
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
//通過JDK調(diào)用對象的finalize方法
jla.invokeFinalize(finalizee);
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
private boolean hasBeenFinalized() {
return (next == this);
}
這個線程用來從queue里獲取Finalizer對象,然后執(zhí)行該對象的runFinalizer方法,該方法會將Finalizer對象從unfinalized隊列里剝離出來,這樣意味著下次GC發(fā)生時就可以將其關(guān)聯(lián)的f對象回收了,最后調(diào)用f類的finalized方法。
至此,F(xiàn)inalizer的整個流程打通了。從頭再捋一遍:
當(dāng)GC發(fā)生時,JVM會判斷f類是否只被Finalizer類引用;
若這個類只被Finalizer對象引用,說明這個對象在不久的將來會被回收,現(xiàn)在可以執(zhí)行他的finalize方法了;
將f類放到Finalizer類的ReferenceQueue中,但這個f類對象其實并沒有被回收,因為Finalizer這個類還對他們保持引用;
GC完成之前,JVM會調(diào)用ReferenceQueue中l(wèi)ock對象的notify方法;
Finalizer的守護線程可能會被喚醒,從Queue中取出對象(remove),執(zhí)行該Finalizer對象的runFinalizer方法(將自己從unfinalized隊列移除,然后執(zhí)行引用對象的finalize方法)
下次GC時回收這個對象。
6.6 Finalizer類的評價
f類因為Finalizer的引用而變成了一個臨時的強引用,即使沒有其他的強引用,還是無法立即被回收;
f類至少經(jīng)歷兩次GC才能被回收,因為只有在FinalizerThread執(zhí)行完了f對象的finalize方法的情況下才有可能被下次GC回收,而有可能期間已經(jīng)經(jīng)歷過多次GC了,但是一直還沒執(zhí)行f對象的finalize方法;
f對象的finalize方法被調(diào)用后,這個對象其實還并沒有被回收,雖然可能在不久的將來會被回收。
Finalizer線程是一個單線程來處理f-queue,雖然可以再啟動第二個(forkSecondaryFinalizer()),但是也是兩個線程而已,如果系統(tǒng)中有很多線程爭用cpu,在系統(tǒng)壓力比較大的情況下,F(xiàn)inalizer線程獲取到cpu時間片的時間是不確定的,在其獲取到時間片之前,應(yīng)該被回收的Finalizer對象一直在隊列中積累,占用大量內(nèi)存,經(jīng)過n次gc后,仍然沒有機會被釋放掉,這些對象都進入到老年代,導(dǎo)致old剩余空間變小,從而使fullgc會更加頻繁,如果Finalizer對象積壓嚴(yán)重的甚至?xí)?dǎo)致oom;
如果Finalizer對象生產(chǎn)的速度比Finalizer線程處理的速度要快,也會導(dǎo)致f-queue隊列里面的Finalizer對象積壓,這些對象一直占用jvm的內(nèi)存,直到oom;
如果執(zhí)行某個f類的finalizer方法執(zhí)行非常耗時,或這個方法里面的操作被鎖阻塞了Finalizer線程,那么就會導(dǎo)致隊列里面其它的Finalizer對象一直在等待隊列里面無法被回收釋放空間,最終導(dǎo)致oom;
Reference對象是在gc的時候來處理的,如果沒有觸發(fā)GC就沒有機會觸發(fā)Reference引用的處理操作,那么應(yīng)該被回收的FinalReference對象就一直在unfinalized隊列里,無法被回收,導(dǎo)致被它引用的對象也無法回收,然后又導(dǎo)致被引用對象占用的資源也不會釋放,最終可能會導(dǎo)致native資源耗盡;
可能導(dǎo)致資源泄露,例如當(dāng)jvm退出時,很可能unfinalizer隊列里的對象沒有被處理完就退出了;
對象有可能在執(zhí)行過finalize方法后,又被強引用引用到了,于是對象就復(fù)活了;
一句話總結(jié):盡量不要使用Finalizer類,釋放資源一定要手動去釋放,如果忘記釋放,依靠finalizer的機制是不靠譜的,很可能會導(dǎo)致一些嚴(yán)重的內(nèi)存問題或native資源泄露問題;如果一定要用,必須保證調(diào)用finalize方法能夠快速執(zhí)行完成。
7、 Cleaner
另外java里面還有一個sun.misc.Cleaner類,它繼承自PhantomReference,作用同F(xiàn)inalize一樣,它的清理工作是在ReferenceHandel線程里面完成的,只是少了Finalizer線程處理這一步,F(xiàn)inalize存在的問題,它基本都有,如果clean方法使用不當(dāng),阻塞ReferenceHander線程,會導(dǎo)致比finalizer線程更加嚴(yán)重的問題。
public class Cleaner
extends PhantomReference<Object>
{
// Dummy reference queue, needed because the PhantomReference constructor
// insists that we pass a queue. Nothing will ever be placed on this queue
// since the reference handler invokes cleaners explicitly.
// 就像英文注釋所說的,這貨沒啥卵用
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
// Doubly-linked list of live cleaners, which prevents the cleaners
// themselves from being GC'd before their referents
// 所有的cleaner都會被加到一個雙向鏈表中去,這樣做是為了保證在referent被回收之前
// 這些Cleaner都是存活的。
static private Cleaner first = null;
private Cleaner
next = null,
prev = null;
// 構(gòu)造的時候把自己加到雙向鏈表中去
private static synchronized Cleaner add(Cleaner cl) {
if (first != null) {
cl.next = first;
first.prev = cl;
}
first = cl;
return cl;
}
// clean方法會調(diào)用remove把當(dāng)前的cleaner從鏈表中刪除。
private static synchronized boolean remove(Cleaner cl) {
// If already removed, do nothing
if (cl.next == cl)
return false;
// Update list
if (first == cl) {
if (cl.next != null)
first = cl.next;
else
first = cl.prev;
}
if (cl.next != null)
cl.next.prev = cl.prev;
if (cl.prev != null)
cl.prev.next = cl.next;
// Indicate removal by pointing the cleaner to itself
cl.next = cl;
cl.prev = cl;
return true;
}
// 用戶自定義的一個Runnable對象,
private final Runnable thunk;
// 私有有構(gòu)造函數(shù),保證了用戶無法單獨地使用new來創(chuàng)建Cleaner。
private Cleaner(Object referent, Runnable thunk) {
super(referent, dummyQueue);
this.thunk = thunk;
}
/**
* 所有的Cleaner都必須通過create方法進行創(chuàng)建。
*/
public static Cleaner create(Object ob, Runnable thunk) {
if (thunk == null)
return null;
return add(new Cleaner(ob, thunk));
}
/**
* 這個方法會被Reference Handler線程調(diào)用,來清理資源。
*/
public void clean() {
if (!remove(this))
return;
try {
thunk.run();
} catch (final Throwable x) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null)
new Error("Cleaner terminated abnormally", x)
.printStackTrace();
System.exit(1);
return null;
}});
}
}
}
Cleaner本身不帶有清理邏輯,所有的邏輯都封裝在thunk參數(shù)中,通過構(gòu)造函數(shù)傳入,因此thunk是怎么實現(xiàn)的才是最關(guān)鍵的。
JDK中的DirectByteBuffer就是使用Cleaner清理的,來看一下它得實現(xiàn):
DirectByteBuffer(int cap) {// package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
//通過unsafe分配內(nèi)存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
//構(gòu)建Cleaner實例
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
private static class Deallocator
implements Runnable
{
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
//通過unsafe釋放內(nèi)存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
可見,DirectBuffer的內(nèi)存釋放通過Cleaner調(diào)用unsafe類來實現(xiàn)。不過DirectBuffer的釋放時機還是不確定的。首先,得發(fā)生GC,其次,Reference Handler得調(diào)度到,然后處理到你的cleaner才行。
如果要實現(xiàn)一個cleaner,萬萬不要在run方法里寫一些執(zhí)行時間很長,或者會阻塞線程的邏輯的,會把Reference Handler拖死。
由于Finalizer存在上文提到的諸多問題,Java 9中finalize方法已經(jīng)被廢棄,新增java.lang.ref.Cleaner類來提供更靈活、有效的資源釋放方式。這個新的 java.lang.ref.Cleaner 其實是以前的 sun.misc.Cleaner 的公有API移植版。Cleaner 是基于 PhantomReference 的,所以不會像finalizer那樣有復(fù)活對象的機會,所以坑會比finalizer稍微少一點。不過,也就是稍微少一點而已。