本文基於 OpenJDK17 進行討論,垃圾回收器為 ZGC。 提示: 為了方便大家索引,特將在上篇文章 《以 ZGC 為例,談一談 JVM 是如何實現 Reference 語義的》 中討論的眾多主題獨立出來。 FinalReference 對於我們來說是一種比較陌生的 Reference 類型,因 ...
本文基於 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
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 中,這個流程就是我們第三小節主要討論的內容,大家還記得嗎 ?
和 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 中。
這個結構是不是和第三小節中我們介紹的 Cleaner 非常相似。
而 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 方法。
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 的語義實現就很簡單了,總體流程是一樣的,只不過在一些地方做了些特殊的處理。
在 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() 方法。這是不是就和上一小節中的內容串起來了。
當 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 對象。