1. 程式人生 > 實用技巧 >Java引用型別之最終引用

Java引用型別之最終引用

FinalReference類只有一個子類Finalizer,並且Finalizer由關鍵字final修飾,所以無法繼承擴充套件。類的定義如下:

class FinalReference<T> extends Reference<T> {
    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

FinalReference是包許可權,開發者無法直接進行繼承擴充套件,不過這個類已經有了一個子類Finalizer,如下:

final class Finalizer extends FinalReference { 

    /* A native method that invokes an arbitrary object's finalize method is
       required since the finalize method is protected
     */
    static native void invokeFinalizeMethod(Object o) throws Throwable;

    private static ReferenceQueue  queue = new ReferenceQueue();
    private static Finalizer       unfinalized = null;
    private static final Object    lock = new Object();

    // 定義的這2個屬性可將Finalizer物件連線成雙向連結串列
    private Finalizer  next = null,
                       prev = null;

    // 私有建構函式,開發者不能建立Finalizer物件
    private Finalizer(Object finalizee) { // FinalReference指向的物件引用
        super(finalizee, queue);
        add();
    }

    /* Invoked by VM */
    static void register(Object finalizee) {
        new Finalizer(finalizee);  // 封裝為Finalizer物件
    }  
    
    // 將當前物件插入到Finalizer物件鏈裡,新插入的this物件放到雙向連結串列的頭部
    // unfinalized是一個靜態欄位,指向連結串列的頭部,所以如果Finalizer類不解除安裝,那麼這個連結串列中的物件永遠都存活
    private void add() {  // 
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }
      
    ...
}

由於建構函式是私有的,所以只能由虛擬機器通過呼叫register()方法將指向的物件封裝為Finalizer物件,那麼需要清楚知道這個指向的物件以及什麼時候呼叫register()方法。   

在類載入的過程中,如果當前類有重寫finalize()方法,則其物件會被封裝為FinalReference物件(稱為finalizer類),這種型別的物件被回收前會先呼叫其finalize()方法。所以finalizer類就是指向的物件。

之前在解析類時,在ClassFileParser::parse_method()方法中有如下判斷邏輯:

if ( name == vmSymbols::finalize_method_name() &&
       signature == vmSymbols::void_method_signature()) {
    if (m->is_empty_method()) {
      _has_empty_finalizer = true;
    } else {
      _has_finalizer = true;
    }
}

每一個方法都會執行這個判斷,如果方法名稱為finalize並且返回型別為void時,如果方法體不為空時,_has_finalizer的值才會更新為true。這樣最終解析完這個Class檔案時會呼叫如下方法:

void ClassFileParser::set_precomputed_flags(instanceKlassHandle k) {
  Klass* super = k->super();

  // Check if this klass has an empty finalize method (i.e. one with return bytecode only),
  // in which case we don't have to register objects as finalizable
  if (!_has_empty_finalizer) {
    if ( _has_finalizer ||
         (super != NULL && super->has_finalizer())
    ){
      k->set_has_finalizer();
    }
  }

只有當重寫的finalize()方法體不為空或者父類就是一個finalizer型別,那麼當前的類也是一個finalizer型別。只有finalizer型別才會呼叫finalize()方法,所以Object類中的finalize()方法不會被呼叫,因為方法體為空。

接著看什麼時候呼叫register()方法,HotSpot可能會在2個時機中的任意一個呼叫Finalizer.register()方法來註冊物件,這個選擇依賴於RegisterFinalizersAtInit這個vm引數是否被設定,預設值為true,也就是在呼叫建構函式返回之前呼叫Finalizer.register()方法,如果通過-XX:-RegisterFinalizersAtInit關閉了該引數,那將在物件空間分配好之後將這個物件註冊進去。

對於第1個時機,我們在介紹類重寫時的Rewriter::rewrite_Object_init()函式時已經介紹過。對於第2個時機,之前介紹過在解析執行時呼叫的TemplateTable::new()函式,當一個類重寫了finalize()方法時,會執行慢速分配,最終會呼叫instanceKlass::allocate_instance()方法,這在之前也已經介紹過,這裡不再介紹。

在HotSpot中,在GC進行可達性分析的時候,如果當前物件是finalizer型別的物件,並且本身不可達(與GC Roots無相連線的引用),則會被加入到一個ReferenceQueue型別的佇列中。而系統在初始化的過程中,會啟動一個FinalizerThread型別的守護執行緒(執行緒名Finalizer),該執行緒會不斷消費ReferenceQueue中的物件,並執行其finalize()方法。物件在執行finalize()方法後,只是斷開了與Finalizer的關聯,並不意味著會立即被回收,還是要等待下一次GC,而每個物件的finalize()方法都只會執行一次,不會重複執行。

// 從ReferenceQueue中獲取物件並執行物件的finalize()方法
private static class FinalizerThread extends Thread {
      ...
      public void run() {
          ...
          final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
          running = true;
          for (;;) {
              try {
                  Finalizer f = (Finalizer)queue.remove(); // 獲取可回收物件
                  f.runFinalizer(jla); // 執行物件的finalize()方法
              } catch (InterruptedException x) {  }
          }
      }
}

在之前說到引用執行緒ReferenceHandler會把pending中儲存的等待被回收的物件加入到引用佇列,這裡就可以從引用佇列中獲取Finalizer物件,然後呼叫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();
}

static native void invokeFinalizeMethod(Object o) throws Throwable;

方法將Finalizer物件從Finalizer物件鏈裡移除出來,這樣意味著下次GC發生的時候就可能將其關聯的finalizer型別物件回收掉,最後將這個Finalizer物件關聯的finalizer型別物件傳給了一個native方法invokeFinalizeMethod(),實現如下:

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");
    if (mid == NULL) return;
    (*env)->CallVoidMethod(env, ob, mid);
}

調了這個finalizer型別物件的finalize()方法。 

相關文章的連結如下:

1、在Ubuntu 16.04上編譯OpenJDK8的原始碼

2、除錯HotSpot原始碼

3、HotSpot專案結構 

4、HotSpot的啟動過程

5、HotSpot二分模型(1)

6、HotSpot的類模型(2)

7、HotSpot的類模型(3)

8、HotSpot的類模型(4)

9、HotSpot的物件模型(5)

10、HotSpot的物件模型(6)

11、操作控制代碼Handle(7)

12、控制代碼Handle的釋放(8)

13、類載入器

14、類的雙親委派機制

15、核心類的預裝載

16、Java主類的裝載

17、觸發類的裝載

18、類檔案介紹

19、檔案流

20、解析Class檔案

21、常量池解析(1)

22、常量池解析(2)

23、欄位解析(1)

24、欄位解析之偽共享(2)

25、欄位解析(3)

26、欄位解析之OopMapBlock(4)

27、方法解析之Method與ConstMethod介紹

28、方法解析

29、klassVtable與klassItable類的介紹

30、計算vtable的大小

31、計算itable的大小

32、解析Class檔案之建立InstanceKlass物件

33、欄位解析之欄位注入

34、類的連線

35、類的連線之驗證

36、類的連線之重寫(1)

37、類的連線之重寫(2)

38、方法的連線

39、初始化vtable

40、初始化itable

41、類的初始化

42、物件的建立

43、Java引用型別

44、Java引用型別之軟引用(1)

45、Java引用型別之軟引用(2)

46、Java引用型別之弱引用與幻像引用

47、Java引用型別之最終引用

作者持續維護的個人部落格classloading.com

關注公眾號,有HotSpot原始碼剖析系列文章!

 

參考:

(1)深入理解JDK中的Reference原理和原始碼實現

(2)JVM原始碼分析之FinalReference完全解讀