java中的引用物件

爬蜥發表於2019-03-24

參考reference 詳解

java中使用Reference物件來描述所有的引用物件

java中的引用物件
referent表示被引用的物件。一個Reference可能有4種狀態:Active、Pending、Enqueued、Inactive
java中的引用物件
在構造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();
}
複製程式碼

則是先看是不是已經執行過,執行過就返回,這裡可以得到如下三點資訊

  1. 物件的finalize()方法只會執行一次
  2. 如果在第一次執行finalize的時候讓物件強行恢復引用,則可以逃過第一次的GC,但是由於第二次不會再執行,此時則會被回收掉
  3. 對於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);
}
複製程式碼

相關文章