FinalReference 如何使 GC 過程變得拖拖拉拉

bin的技术小屋發表於2024-06-17

本文基於 OpenJDK17 進行討論,垃圾回收器為 ZGC。

提示: 為了方便大家索引,特將在上篇文章 《以 ZGC 為例,談一談 JVM 是如何實現 Reference 語義的》 中討論的眾多主題獨立出來。


FinalReference 對於我們來說是一種比較陌生的 Reference 型別,因為我們好像在各大中介軟體以及 JDK 中並沒有見過它的應用場景,事實上,FinalReference 被設計出來的目的也不是給我們用的,而是給 JVM 用的,它和 Java 物件的 finalize() 方法執行機制有關。

public class Object {
    @Deprecated(since="9")
    protected void finalize() throws Throwable { }
}

我們看到 finalize() 方法在 OpenJDK9 中已經被標記為 @Deprecated 了,並不推薦使用。筆者其實一開始也並不想提及它,但是思來想去,本文是主要介紹各類 Refernce 語義實現的,前面筆者已經非常詳細的介紹了 SoftReference,WeakReference,PhantomReference 在 JVM 中的實現。

在文章的最後何不利用這個 FinalReference 將前面介紹的內容再次為大家串聯一遍,加深一下大家對 Reference 整個處理鏈路的理解,基於這個目的,才有了本小節的內容。但筆者的本意並不是為了讓大家使用它。

下面我們還是按照老規矩,繼續從 JDK 以及 JVM 這兩個視角全方位的介紹一下 FinalReference 的實現機制,併為大家解釋一下這個 FinalReference 如何使整個 GC 過程變得拖拖拉拉,磨磨唧唧~~~

1. 從 JDK 視角看 FinalReference

image

FinalReference 本質上來說它也是一個 Reference,所以它的基本語義和 WeakReference 保持一致,JVM 在 GC 階段對它的整體處理流程和 WeakReference 也是大致一樣的。

唯一一點不同的是,由於 FinalReference 是和被它引用的 referent 物件的 finalize() 執行有關,當一個普通的 Java 物件在整個 JVM 堆中只有 FinalReference 引用它的時候,按照 WeakReference 的基礎語義來講,這個 Java 物件就要被回收了。

但是在這個 Java 物件被回收之前,JVM 需要保證它的 finalize()被執行到,所以 FinalReference 會再次將這個 Java 物件重新標記為 alive,也就是在 GC 階段重新復活這個 Java 物件。

後面的流程就和其他 Reference 一樣了,FinalReference 也會被 JVM 加入到 _reference_pending_list 連結串列中,ReferenceHandler 執行緒被喚醒,隨後將這個 FinalReference 從 _reference_pending_list 上摘下,並加入到與其關聯的 ReferenceQueue 中,這個流程就是我們第三小節主要討論的內容,大家還記得嗎 ?

image

和 Cleaner 不同的是,對於 FinalReference 來說,在 JDK 中還有一個叫做 FinalizerThread 執行緒來專門處理它,FinalizerThread 執行緒會不斷的從與 FinalReference 關聯的 ReferenceQueue 中,將所有需要被處理的 FinalReference 摘下,然後挨個執行被它所引用的 referent 物件的 finalize() 方法。

隨後在下一輪的 GC 中,FinalReference 物件以及它引用的 referent 物件才會被 GC 回收掉。

以上就是 FinalReference 被 JVM 處理的整個生命週期,下面讓我們先回到最初的起點,這個 FinalReference 是怎麼和一個 Java 物件關聯起來的呢 ?

我們知道 FinalReference 是和 Java 物件的 finalize() 方法執行有關的,如果一個 Java 類沒有重寫 finalize() 方法,那麼在建立這個 Java 類的例項物件的時候將不會和這個 FinalReference 有任何的瓜葛,它就是一個普通的 Java 物件。

但是如何一個 Java 類重寫了 finalize() 方法 ,那麼在建立這個 Java 類的例項物件的時候, JVM 就會將一個 FinalReference 例項和這個 Java 物件關聯起來。

instanceOop InstanceKlass::allocate_instance(TRAPS) {
  // 判斷這個類是否重寫了 finalize() 方法
  bool has_finalizer_flag = has_finalizer(); 
  instanceOop i;
  // 建立例項物件
  i = (instanceOop)Universe::heap()->obj_allocate(this, size, CHECK_NULL);
  // 如果該物件重寫了  finalize() 方法
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    // JVM 這裡就會呼叫 Finalizer 類的靜態方法 register
    // 將這個 Java 物件與 FinalReference 關聯起來
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

我們看到,在 JVM 建立物件例項的時候,會首先透過 has_finalizer() 方法判斷這個 Java 類有沒有重寫 finalize() 方法,如果重寫了就會呼叫 register_finalizer 方法,JVM 最終會呼叫 JDK 中的 Finalizer 類的靜態方法 register。

final class Finalizer extends FinalReference<Object> {
    static void register(Object finalizee) {
        new Finalizer(finalizee);
    }
}

在這裡 JVM 會將剛剛建立出來的普通 Java 物件 —— finalizee,與一個 Finalizer 物件關聯起來, Finalizer 物件的型別正是 FinalReference 。這裡我們可以看到,當一個 Java 類重寫了 finalize() 方法的時候,每當建立一個該類的例項物件,JVM 就會自動建立一個對應的 Finalizer 物件

Finalizer 的整體設計和之前介紹的 Cleaner 非常相似,不同的是 Cleaner 是一個 PhantomReference,而 Finalizer 是一個 FinalReference。

它們都有一個 ReferenceQueue,只不過 Cleaner 中的那個基本沒啥用,但是 Finalizer 中的這個 ReferenceQueue 卻有非常重要的作用。

它們內部都有一個雙向連結串列,裡面包含了 JVM 堆中所有的 Finalizer 物件,用來確保這些 Finalizer 在執行 finalizee 物件的 finalize() 方法之前不會被 GC 回收掉。

final class Finalizer extends FinalReference<Object> { 

    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();

    // 雙向連結串列,儲存 JVM 堆中所有的 Finalizer 物件,防止 Finalizer 被 GC 掉
    private static Finalizer unfinalized = null;

    private Finalizer next, prev;

    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        // push onto unfinalized
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }
}

在建立 Finalizer 物件的時候,首先會呼叫父類方法,將被引用的 Java 物件以及 ReferenceQueue 關聯註冊到 FinalReference 中。

    Reference(T referent, ReferenceQueue<? super T> queue) {
        // 被引用的普通 Java 物件
        this.referent = referent;
        //  Finalizer 中的 ReferenceQueue 例項(全域性)
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

最後將這個 Finalizer 物件插入到雙向連結串列 —— unfinalized 中。

image

這個結構是不是和第三小節中我們介紹的 Cleaner 非常相似。

image

而 Cleaner 最後是被 ReferenceHandler 執行緒執行的,那這個 Finalizer 最後是被哪個執行緒執行的呢 ?

這裡就要引入另一個 system thread 了,在 Finalizer 類初始化的時候會建立一個叫做 FinalizerThread 的執行緒。

final class Finalizer extends FinalReference<Object> { 
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        // 獲取 system thread group
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        // 建立 system thread : FinalizerThread
        Thread finalizer = new FinalizerThread(tg);
        finalizer.setPriority(Thread.MAX_PRIORITY - 2);
        finalizer.setDaemon(true);
        finalizer.start();
    }
}

FinalizerThread 的優先順序被設定為 Thread.MAX_PRIORITY - 2,還記得 ReferenceHandler 執行緒的優先順序嗎 ?

public abstract class Reference<T> {

    static {
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        // 設定 ReferenceHandler 執行緒的優先順序為最高優先順序
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();  
    }
}

而一個普通的 Java 執行緒,它的預設優先順序是多少呢 ?

    /**
     * The default priority that is assigned to a thread.
     */
    public static final int NORM_PRIORITY = 5;

我們可以看出這三類執行緒的排程優先順序為:ReferenceHandler > FinalizerThread > Java 業務 Thead

FinalizerThread 執行緒在執行起來之後,會不停的從一個 queue 中獲取 Finalizer 物件,然後執行 Finalizer 中的 runFinalizer 方法,這個邏輯是不是和 ReferenceHandler 執行緒不停的從 _reference_pending_list 中獲取 Cleaner 物件,然後執行 Cleaner 的 clean 方法非常相似。

    private static class FinalizerThread extends Thread {

        public void run() {
            for (;;) {
                try {
                    Finalizer f = (Finalizer)queue.remove();
                    f.runFinalizer(jla);
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }
        }
    }

這個 queue 就是 Finalizer 中定義的 ReferenceQueue,在 JVM 建立 Finalizer 物件的時候,會將重寫了 finalize() 方法的 Java 物件與這個 ReferenceQueue 一起註冊到 FinalReference 中。

final class Finalizer extends FinalReference<Object> { 
    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
    private Finalizer(Object finalizee) {
        super(finalizee, queue);
    }
}

那這個 ReferenceQueue 中的 Finalizer 物件是從哪裡新增進來的呢 ?這就又和我們第三小節中介紹的內容遙相呼應起來了,就是 ReferenceHandler 執行緒新增進來的。

private static class ReferenceHandler extends Thread {
    private static void processPendingReferences() {
        // ReferenceHandler 執行緒等待 JVM 向 _reference_pending_list 填充 Reference 物件
        waitForReferencePendingList();
        // 用於指向 JVM 的 _reference_pending_list
        Reference<?> pendingList;
        synchronized (processPendingLock) {
            // 獲取 _reference_pending_list,隨後將 _reference_pending_list 置為 null
            // 方便 JVM 在下一輪 GC 處理其他 Reference 物件
            pendingList = getAndClearReferencePendingList();
        }
        // 將 pendingList 中的 Reference 物件挨個從連結串列中摘下處理
        while (pendingList != null) {
            // 從 pendingList 中摘下 Reference 物件
            Reference<?> ref = pendingList;
            pendingList = ref.discovered;
            ref.discovered = null;
            
            // 如果該 Reference 物件是 Cleaner 型別,那麼在這裡就會呼叫它的 clean 方法
            if (ref instanceof Cleaner) {
                 // Cleaner 的 clean 方法就是在這裡呼叫的
                ((Cleaner)ref).clean();
            } else {
                // 這裡處理除 Cleaner 之外的其他 Reference 物件
                // 比如,其他 PhantomReference,WeakReference,SoftReference,FinalReference
                // 將他們新增到各自注冊的 ReferenceQueue 中
                ref.enqueueFromPending();
            }
        }
    }
}

當一個 Java 物件在 JVM 堆中只有 Finalizer 物件引用,除此之外沒有任何強引用或者軟引用之後,JVM 首先會將這個 Java 物件復活,在本次 GC 中並不會回收它,隨後會將這個 Finalizer 物件插入到 JVM 內部的 _reference_pending_list 中,然後從 waitForReferencePendingList() 方法上喚醒 ReferenceHandler 執行緒。

ReferenceHandler 執行緒將 _reference_pending_list 中的 Reference 物件挨個摘下,注意 _reference_pending_list 中儲存的既有 Cleaner,也有其他的 PhantomReference,WeakReference,SoftReference,當然也有本小節的 Finalizer 物件。

如果摘下的是 Cleaner 物件那麼就執行它的 clean 方法,如果是其他 Reference 物件,比如這裡的 Finalizer,那麼就透過 ref.enqueueFromPending(),將這個 Finalizer 物件插入到它的 ReferenceQueue 中。

當這個 ReferenceQueue 有了 Finalizer 物件之後,FinalizerThread 執行緒就會被喚醒,然後執行 Finalizer 物件的 runFinalizer 方法。

image

Finalizer 的內部有一個雙向連結串列 —— unfinalized,它儲存了當前 JVM 堆中所有的 Finalizer 物件,目的是為了避免在執行其引用的 referent 物件的 finalize() 方法之前被 GC 掉。

在 runFinalizer 方法中首先要做的就是將這個 Finalizer 物件從雙向連結串列 unfinalized 上摘下,然後執行 referent 物件的 finalize() 方法。這裡我們可以看到,大家在 Java 類中重寫的 finalize() 方法就是在這裡被執行的。

    private void runFinalizer(JavaLangAccess jla) {
        synchronized (lock) {
            if (this.next == this)      // already finalized
                return;
            // 將 Finalizer 物件從雙向連結串列 unfinalized 上摘下
            if (unfinalized == this)
                unfinalized = this.next;
            else
                this.prev.next = this.next;
            if (this.next != null)
                this.next.prev = this.prev;
            this.prev = null;
            this.next = this;           // mark as finalized
        }

        try {
            // 獲取 Finalizer 引用的 Java 物件
            Object finalizee = this.get();

            if (!(finalizee instanceof java.lang.Enum)) {
                // 執行 java 物件的 finalize() 方法
                jla.invokeFinalize(finalizee);
            }
        } catch (Throwable x) { }
        // 呼叫 FinalReference 的 clear 方法,將其引用的 referent 物件置為 null
        // 下一輪 gc 的時候這個  FinalReference 以及它的 referent 物件就會被回收掉了。
        super.clear();
    }

最後呼叫 Finalizer 物件(FinalReference型別)的 clear 方法,將其引用的 referent 物件置為 null , 在下一輪 GC 的時候, 這個 Finalizer 物件以及它的 referent 物件就會被 GC 掉。

2. 從 JVM 視角看 FinalReference

現在我們已經從 JVM 的外圍熟悉了 JDK 處理 FinalReference 的整個流程,本小節,筆者將繼續帶著大家深入到 JVM 的內部,看看在 GC 的時候,JVM 是如何處理 FinalReference 的。

在本文 5.1 小節中,筆者為大家介紹了 ZGC 在 Concurrent Mark 階段如何處理 Reference 的整個流程,只不過當時我們偏重於 Reference 基礎語義的實現,還未涉及到 FinalReference 的處理。

但我們在明白了 Reference 基礎語義的基礎之上,再來看 FinalReference 的語義實現就很簡單了,總體流程是一樣的,只不過在一些地方做了些特殊的處理。

image

在 ZGC 的 Concurrent Mark 階段,當 GC 執行緒遍歷標記到一個 FinalReference 物件的時候,首先會透過 should_discover 方法來判斷是否應該將這個 FinalReference 物件插入到 _discovered_list 中。判斷邏輯如下:

bool ZReferenceProcessor::should_discover(oop reference, ReferenceType type) const {
  // 獲取 referent 物件的地址檢視
  volatile oop* const referent_addr = reference_referent_addr(reference);
  // 調整 referent 物件的檢視為 remapped + mark0 也就是 weakgood 檢視
  // 獲取 FinalReference 引用的 referent 物件
  const oop referent = ZBarrier::weak_load_barrier_on_oop_field(referent_addr);

  // 如果 Reference 的狀態就是 inactive,那麼這裡將不會重複將 Reference 新增到 _discovered_list 重複處理
  if (is_inactive(reference, referent, type)) {
    return false;
  }
  // referent 還被強引用關聯,那麼 return false 也就是說不能被加入到 discover list 中
  if (is_strongly_live(referent)) {
    return false;
  }
  // referent 還被軟引用有效關聯,那麼 return false 也就是說不能被加入到 discover list 中
  if (is_softly_live(reference, type)) {
    return false;
  }

  return true;
}

首先獲取這個 FinalReference 物件所引用的 referent 物件,如果這個 referent 物件在 JVM 堆中已經沒有任何強引用或者軟引用了,那麼就會將 FinalReference 物件插入到 _discovered_list 中。

但是在插入之前還要透過 is_inactive 方法判斷一下這個 FinalReference 物件是否在上一輪 GC 中被處理過了,

bool ZReferenceProcessor::is_inactive(oop reference, oop referent, ReferenceType type) const {
  if (type == REF_FINAL) {
    return reference_next(reference) != NULL;
  } else {
    return referent == NULL;
  }
}

對於 FinalReference 來說,inactive 的標誌是它的 next 欄位不為空。

public abstract class Reference<T> {
   volatile Reference next;
}

這裡的 next 欄位是幹嘛的呢 ?比如說,這個 FinalReference 物件在上一輪的 GC 中已經被處理過了,那麼在發生本輪 GC 之前,ReferenceHandler 執行緒就已經將這個 FinalReference 插入到一個 ReferenceQueue 中,這個 ReferenceQueue 是哪來的呢 ?

正是上小節中我們介紹的,JVM 建立 Finalizer 物件的時候傳入的這個 queue。

final class Finalizer extends FinalReference<Object> { 
    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
    private Finalizer(Object finalizee) {
        super(finalizee, queue);
    }
}

而 ReferenceQueue 中的 FinalReference 物件就是透過它的 next 欄位連結起來的,當一個 FinalReference 物件被 ReferenceHandler 執行緒插入到 ReferenceQueue 中之後,它的 next 欄位就不為空了,也就是說一個 FinalReference 物件一旦進入 ReferenceQueue,它的狀態就變為 inactive 了。

那麼在下一輪的 GC 中如果一個 FinalReference 物件的狀態是 inactive,表示它已經被處理過了,那麼就不在重複新增到 _discovered_list 中了。

如果一個 FinalReference 物件之前沒有被處理過,並且它引用的 referent 物件當前也沒有任何強引用或者軟引用關聯,那麼是不是說明這個 referent 就該被回收了 ?想想 FinalReference 的語義是什麼 ? 是不是就是在 referent 物件被回收之前還要呼叫它的 finalize() 方法 。

所以為了保證 referent 物件的 finalize() 方法得到呼叫,JVM 就會在 discover 方法中將其復活。隨後會將 FinalReference 物件插入到 _discovered_list 中,這樣在 GC 之後 ,FinalizerThread 就會呼叫 referent 物件的 finalize() 方法了,這裡是不是和上一小節的內容呼應起來了。

void ZReferenceProcessor::discover(oop reference, ReferenceType type) {
  // 復活 referent 物件
  if (type == REF_FINAL) {
    // 獲取 referent 地址檢視
    volatile oop* const referent_addr = reference_referent_addr(reference);
    // 如果是 FinalReference 那麼就需要對 referent 進行標記,檢視改為 finalizable 表示只能透過 finalize 方法才能訪問到 referent 物件
    // 因為 referent 後續需要透過 finalize 方法被訪問,所以這裡需要對它進行標記,不能回收
    ZBarrier::mark_barrier_on_oop_field(referent_addr, true /* finalizable */);
  }

  // Add reference to discovered list
  // 確保 reference 不在 _discovered_list 中,不能重複新增
  assert(reference_discovered(reference) == NULL, "Already discovered");
  oop* const list = _discovered_list.addr();
  // 頭插法,reference->discovered = *list
  reference_set_discovered(reference, *list);
  // reference 變為 _discovered_list 的頭部
  *list = reference;
}

那麼 JVM 如何將一個被 FinalReference 引用的 referent 物件復活呢 ?

uintptr_t ZBarrier::mark_barrier_on_finalizable_oop_slow_path(uintptr_t addr) {
  // Mark,這裡的 Finalizable = true
  return mark<GCThread, Follow, Finalizable, Overflow>(addr);
}
template <bool gc_thread, bool follow, bool finalizable, bool publish>
uintptr_t ZBarrier::mark(uintptr_t addr) {
  uintptr_t good_addr;

  // Mark,在 _livemap 標記點陣圖中將 referent 對應的 bit 位標記為 1
  if (should_mark_through<finalizable>(addr)) {
    ZHeap::heap()->mark_object<gc_thread, follow, finalizable, publish>(good_addr);
  }

  if (finalizable) {
    // 調整 referent 物件的檢視為 finalizable
    return ZAddress::finalizable_good(good_addr);
  }

  return good_addr;
}

其實很簡單,首先透過 ZPage::mark_object 將 referent 對應在標記點陣圖 _livemap 的 bit 位標記為 1。其次調整 referent 物件的地址檢視為 finalizable,表示該物件在回收階段被 FinalReference 復活。

inline bool ZPage::mark_object(uintptr_t addr, bool finalizable, bool& inc_live) {
  // Set mark bit, 獲取 referent 物件在標記點陣圖的索引 index 
  const size_t index = ((ZAddress::offset(addr) - start()) >> object_alignment_shift()) * 2;
  // 將 referent 對應的 bit 位標記為 1
  return _livemap.set(index, finalizable, inc_live);
}

到現在 FinalReference 物件已經被加入到 _discovered_list 中了,referent 物件也被複活了,隨後在 ZGC 的 Concurrent Process Non-Strong References 階段,JVM 就會將 _discovered_list 中的所有 Reference 物件(包括這裡的 FinalReference)統統轉移到 _reference_pending_list 中,並喚醒 ReferenceHandler 執行緒去處理。

隨後 ReferenceHandler 執行緒將 _reference_pending_list 中的 FinalReference 物件在新增到 Finalizer 中的 ReferenceQueue 中。隨即 FinalizerThread 執行緒就會被喚醒,然後執行 Finalizer 物件的 runFinalizer 方法,最終就會執行到 referent 物件的 finalize() 方法。這是不是就和上一小節中的內容串起來了。

image

當 referent 物件的 finalize() 方法被 FinalizerThread 執行完之後,下一輪 GC 的這時候,這個 referent 物件以及與它關聯的 FinalReference 物件就會一起被 GC 回收了。

總結

從整個 JVM 對於 FinalReference 的處理過程可以看出,只要我們在一個 Java 類中重寫了 finalize() 方法,那麼當這個 Java 類對應的例項可以被回收的時候,它的 finalize() 方法是一定會被呼叫的。

呼叫的時機取決於 FinalizerThread 執行緒什麼時候被 OS 排程到,但是從另外一個側面也可以看出,由於 FinalReference 的影響,一個原本該被回收的物件,在 GC 的過程又會被 JVM 復活。而只有當這個物件的 finalize() 方法被呼叫之後,該物件以及與它關聯的 FinalReference 只能等到下一輪 GC 的時候才能被回收。

如果 finalize() 方法執行的很久又或者是 FinalizerThread 沒有被 OS 排程到,這中間可能已經發生好幾輪 GC 了,那麼在這幾輪 GC 中,FinalReference 和他的 referent 物件就一直不會被回收,表現的現象就是 JVM 堆中存在大量的 Finalizer 物件。

相關文章