概述
Java的引用分為StrongReference、SoftReference、WeakReference、PhantomReference和FinalReference;
其中StrongRference并沒有定義成接口,而其余四種都有相應的接口定義;
這五種引用的使用場景如下:
- 強引用
最普遍的引用。如果一個對象具有強引用,垃圾回收器絕不會回收它。當內存空 間不足,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。 - 軟引用
如果內存空間足,則不回收;如果內存空間不足,則會被回收;通常可以用來做為緩存; - 弱引用
一旦發生GC,弱引用對象立即被回收; - 虛引用
虛引用不會影響對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收;
SoftReference和WeakReference
上面提到SoftReference適合用來實現緩存,例如google的guava cache就使用到了WeakReference和SoftReference,在創建cache時可以通過如下方式指定用哪種引用:
//使用WeakReference
CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES).weakKeys().weakValues()
.build();
//使用SoftReference
CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES).softValues()
.build();
創建SoftReference和WeakReference的構造函數如下:
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
可以看到可以傳入一個ReferenceQueue對象,那么這個傳入到Queue對象到底有什么用呢?如果WeakReference所引用的對象被垃圾回收,Java虛擬機就會把這個WeakReference加入到與之關聯的Queue中;
PhantomReference
DirectByteBuffer是JDK提供的堆外內存對象,其構造函數如下:
DirectByteBuffer(int cap) {
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 {
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;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
其中的Cleaner對象繼承自PhantomReference:
public class Cleaner extends PhantomReference<Object>
那么定義成PhantomReference起什么作用呢?其實主要是為了回收堆外內存,關于其具體的實現,后面會進行介紹;
FinalReference
FinalReference有個子類Finalizer,這兩個類的包訪問級別都是default而不是public的,因此我們無法在代碼中直接調用;
說到FinalReference的使用,就免不了談及對象的創建:
instanceOop InstanceKlass::allocate_instance(TRAPS) {
bool has_finalizer_flag = has_finalizer(); //判斷當前類是否包含非空的finalize方法
int size = size_helper(); //確定要分配的內存大小
KlassHandle h_k(THREAD, this);
instanceOop i;
i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
if (has_finalizer_flag && !RegisterFinalizersAtInit) {
i = register_finalizer(i, CHECK_NULL);
}
return i;
}
instanceOop InstanceKlass::register_finalizer(instanceOop i, TRAPS) {
if (TraceFinalizerRegistration) {
tty->print("Registered ");
i->print_value_on(tty);
tty->print_cr(" (" INTPTR_FORMAT ") as finalizable", (address)i);
}
instanceHandle h_i(THREAD, i);
// Pass the handle as argument, JavaCalls::call expects oop as jobjects
JavaValue result(T_VOID);
JavaCallArguments args(h_i);
methodHandle mh (THREAD, Universe::finalizer_register_method());
JavaCalls::call(&result, mh, &args, CHECK_NULL);
return h_i();
}
可以看到對象創建時,如果發現對象實現了finalize方法,則會調用Finalizer.register方法,將對象注冊到Finalizer的靜態屬性unfinalized上,形成Finalizer鏈表;
final class Finalizer extends FinalReference<Object> {
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
private static Finalizer unfinalized = null;
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
if (running)
return;
// Finalizer thread starts before System.initializeSystemClass
// is called. Wait until JavaLangAccess is available
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 {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
}
}
可以看到FinalizerThread線程會從queue隊列中獲取Finalizer對象,調用其finalize方法;queue即創建Reference時傳入的queue對象;
那么,Finalizer什么時候會加入到queue隊列呢?很容易想到,當GC發生時,Finalizer對象會加入到queue隊列;
那么JVM是如何實現當對象回收時,將對象添加到queue呢?
# Reference
Reference對象是上述對象的基類,它的結構如下:
private T referent;
volatile ReferenceQueue<? super T> queue;//對象被回收后,Reference會被放入隊列
Reference next;
transient private Reference<T> discovered; /* used by VM */
private static Reference<Object> pending = null;//JVM GC時,會將Reference對象設置到pending上,discovered相當于next,指向下一個Reference對象
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
c = r instanceof Cleaner ? (Cleaner) r : null;
pending = r.discovered;
r.discovered = null;
} else {
if (waitForNotify) {//如果等待,則調用lock.wait方法,當GC時將Reference添加到pending隊列時,會調用lock.notify方法,
lock.wait();
}
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
Thread.yield();
return true;
} catch (InterruptedException x) {
return true;
}
if (c != null) {//如果是Cleaner對象,調用其clean方法釋放資源
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
前面提到的DirectByteBuffer內存的釋放就是通過JVM GC時將對象添加到pending對象,而Reference中的ReferenceHandler線程會將pending中的對象添加到ReferenceQueue,同時如果發現是Cleaner對象,會調用clean方法釋放堆外內存;
對于Finalizer對象,當被添加到ReferenceQueue時,會通過FinalizerThread線程調用finalize方法;因此可以看到Finalizer對象至少要經歷兩次垃圾回收才能被回收,同時由于FinalizerThread線程的優先級為Thread.MAX_PRIORITY - 2,因此當系統壓力較大時,Finalizer對象可能要很久才能被回收;