java中使用Reference物件來描述所有的引用物件
referent表示被引用的物件。一個Reference可能有4種狀態:Active、Pending、Enqueued、Inactive 在構造Reference時可以決定是否指定ReferenceQueue
,會有不同的狀態變更,另外一旦狀態變成Inactive,狀態就不會再做任何變更
ReferenceQueue 與 Reference 之間的合作
當GC發生時,被回收的物件會新增到Pending列表中,通過Reference的next欄位來構建Pending連結串列。在Reference的靜態程式碼塊,則會啟動RefenrenceHandler
static {
...
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
...
}
複製程式碼
並設定為最大的優先順序,它負責將pending中的元素新增到ReferenceQueue,
private static class ReferenceHandler extends Thread {
public void run() {
for (;;) {
Reference r;
synchronized (lock) {
if (pending != null) {
r = pending;
Reference rn = r.next;
pending = (rn == r) ? null : rn;
r.next = r;
} else {
try {
lock.wait();
} catch (InterruptedException x) { }
continue;
}
}
// Fast path for cleaners
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
//存在ReferenceQueue時,將pending中的元素入佇列
ReferenceQueue q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
複製程式碼
ReferenceQueue提供對列的功能,出隊和入隊,當ReferenceQueue作為引數被提供時,這意味著使用者一旦從ReferenceQueue中獲取到元素,也就可以知道,這個物件要被回收了
,以此達到一種通知的效果
強引用、軟引用、弱引用與虛引用
- 強引用。比如通過
new
生成的物件,這類可確保不會被GC回收掉 - 軟引用。一旦記憶體即將溢位,就把這類物件都回收掉,適用於記憶體敏感的快取使用
- 弱引用。每次垃圾回收都可以回收這些引用物件
- 虛引用。與物件的生存無關,僅提供通知機制
虛引用一定要提供ReferenceQueue,因為它無法返回引用為null,如果不提供,那麼連通知的機制都無法實現了
軟引用回收策略細節
軟引用不僅考慮記憶體,還會考慮referent的使用情況和建立時間來決定是否該回收。Hotspot會讀取當前堆剩餘的記憶體,以及配置引數XX:SoftRefLRUPolicyMSPerMB
(每M資料應該存活的毫秒數)
void LRUMaxHeapPolicy::setup() {
size_t max_heap = MaxHeapSize;
max_heap -= Universe::get_heap_used_at_last_gc();
max_heap /= M;
//剩餘空間能夠存的以M為單位的資料應該存活的時間
_max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
assert(_max_interval >= 0,"Sanity check");
}
// The oop passed in is the SoftReference object, and not
// the object the SoftReference points to.
bool LRUCurrentHeapPolicy::should_clear_reference(oop p,
jlong timestamp_clock) {
jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
assert(interval >= 0, "Sanity check");
// The interval will be zero if the ref was accessed since the last scavenge/gc.
if(interval <= _max_interval) {
return false;
}
return true;
}
複製程式碼
軟引用自身攜帶timestamp和clock,其中clock由GC更新,timestamp每次get的時候,如果和clock不一致則更新
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
複製程式碼
如果再上一次GC之後,有過訪問記錄,那麼當前的GC肯定不會回收軟引用,這也就意味著,軟引用如果一直沒有回收,升級到老年代,在OOM之前,有可能出現頻繁的Full GC
WeakHashMap 對弱引用的使用
weakHashMap在 get/put/remove/resize等方法中均使用了expungeStaleEntries
,去掉多餘的資訊
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
...
private void expungeStaleEntries() {
//從註冊的佇列中拿到了值
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
Entry<K,V> e = (Entry<K,V>) x;
...
if (p == e) {
e.value = null; // Help GC
size--;
...
}
}
}
}
複製程式碼
如果從註冊的佇列中拿到了對應的元素,那麼就自動刪掉,這裡就是利用了ReferenceQueue承擔通知的角色,以及弱引用的GC就回收性質
Cleaner與native記憶體回收
在ReferenceHandler中注意到,如果pending中的Reference是一個Cleaner,則直接執行clean
public void clean() {
if (!remove(this))
return;
...
//之前沒有執行過要clean的,現在執行
thunk.run();
...
}
}
複製程式碼
以DirectByteBuffer為例,在使用時,就會建立一個Cleaner
DirectByteBuffer(int cap) {
...
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
private static class Deallocator
implements Runnable
{
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
複製程式碼
java中通過ByteBuffer.allocateDirect
就可以建立,如果DirectByteBuffer被回收,此時唯一引用DirectByteBuffer的是一個虛引用,由於垃圾回收的作用,DirectByteBuffer會處於pending狀態,觸發Native記憶體的回收釋放
參考直接記憶體
延伸一點網路讀寫過程非直接記憶體轉換成直接記憶體的行為,javaNio中寫資料IOUtil.write
實現中可以看到
static long write(FileDescriptor fd, ByteBuffer[] bufs, int offset, int length,
NativeDispatcher nd){
...
if (!(buf instanceof DirectBuffer)) {
//分配直接記憶體
ByteBuffer shadow = Util.getTemporaryDirectBuffer(rem);
shadow.put(buf);
shadow.flip();
vec.setShadow(iov_len, shadow);
buf.position(pos); // temporarily restore position in user buffer
buf = shadow;
pos = shadow.position();
}
...
}
複製程式碼
會發現如果要將一個byte陣列物件傳給native,會先轉換成直接記憶體再操作,這是因為native程式碼訪問陣列必須保證訪問的時候,byte[]物件不能移動,也就是被"pin"釘住,此時要麼是暫停GC(GC演算法有可能要移動物件),要麼是假設換成native的消耗是可接受的,而且I/O操作都很慢,這裡就選擇了後者
Finalizer
Finalizer自身會啟動一個執行緒,它自己的工作就是一直從ReferenceQueue中拉取對應的元素並執行它的runFinalizer方法
private static class FinalizerThread extends Thread {
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer();
} catch (InterruptedException x) {
continue;
}
}
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();
}
複製程式碼
值得注意的是,Finalizer它本身的建構函式是private,只能通過虛擬機器自身來執行register操作,具體的時機根據RegisterFinalizersAtInit
引數來決定,如果值為true,那麼在建構函式返回之前呼叫註冊
//vmSymbols.hpp
...
template(object_initializer_name, "<init>")
...
do_intrinsic(_Object_init, java_lang_Object, object_initializer_name, void_method_signature, F_R) \
//c1_GraphBuilder.cpp
void GraphBuilder::method_return(Value x) {
//RegisterFinalizersAtInit為true
if (RegisterFinalizersAtInit &&
method()->intrinsic_id() == vmIntrinsics::_Object_init) {
call_register_finalizer();
}
複製程式碼
否則在分配好空間物件之後,再註冊
instanceOop instanceKlass::allocate_instance(TRAPS) {
assert(!oop_is_instanceMirror(), "wrong allocation path");
bool has_finalizer_flag = has_finalizer(); // Query before possible GC
int size = size_helper(); // Query before forming handle.
KlassHandle h_k(THREAD, as_klassOop());
instanceOop i;
i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
if (has_finalizer_flag && !RegisterFinalizersAtInit) {
//RegisterFinalizersAtInit為false
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);
//finalizer_register_method即通過Finalizer找到的register
methodHandle mh (THREAD, Universe::finalizer_register_method());
JavaCalls::call(&result, mh, &args, CHECK_NULL);
return h_i();
}
複製程式碼
runFinalizer執行如下
private void runFinalizer() {
synchronized (this) {
if (hasBeenFinalized()) return;
remove();
}
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
invokeFinalizeMethod(finalizee);
/* Clear stack slot containing this variable, to decrease
the chances of false retention with a conservative GC */
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
複製程式碼
則是先看是不是已經執行過,執行過就返回,這裡可以得到如下三點資訊
- 物件的
finalize()方法只會執行一次
。 - 如果在第一次執行finalize的時候讓物件強行恢復引用,則可以逃過第一次的GC,但是由於第二次不會再執行,此時則會被回收掉
- 對於Finalizer物件本身,由於它存在內部的
unfinalized
物件構建的強引用,第一次GC執行,只是在等待runFinalizer的執行,如果執行了,並且之前沒有執行過才會從unfinalized
列表中進行刪掉,從而不可達,再第二次GC的時候回收了Finalizer本身
執行finalize()方法具體細節如下
JNIEXPORT void JNICALL
Java_java_lang_ref_Finalizer_invokeFinalizeMethod(JNIEnv *env, jclass clazz,
jobject ob)
{
jclass cls;
jmethodID mid;
cls = (*env)->GetObjectClass(env, ob);
if (cls == NULL) return;
mid = (*env)->GetMethodID(env, cls, "finalize", "()V");
//沒有finalize什麼都不做
if (mid == NULL) return;
//執行
(*env)->CallVoidMethod(env, ob, mid);
}
複製程式碼