PhantomReference 和 WeakReference 究竟有何不同

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

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

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


PhantomReference 和 WeakReference 如果僅僅從概念上來說其實很難區別出他們之間究竟有何不同,比如, PhantomReference 是用來跟蹤物件是否被垃圾回收的,如果物件被 GC ,那麼其對應的 PhantomReference 就會被加入到一個 ReferenceQueue 中,這個 ReferenceQueue 是在建立 PhantomReference 物件的時候註冊進去的。

我們在應用程式中可以透過檢查這個 ReferenceQueue 中的 PhantomReference 物件,從而可以判斷出其引用的 referent 物件已經被回收,隨即可以做一些釋放資源的工作。

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

而 WeakReference 的概念是,如果一個物件在 JVM 堆中已經沒有任何強引用鏈或者軟引用鏈了,在只有一個 WeakReference 引用它的情況下,那麼這個物件就會被 GC,與其對應的 WeakReference 也會被加入到其註冊的 ReferenceQueue 中。後面的套路和 PhantomReference 一模一樣。

既然兩者在概念上都差不多,JVM 處理的過程也差不多,那麼 PhantomReference 可以用來跟蹤物件是否被垃圾回收,WeakReference 可不可以跟蹤呢 ?

事實上,在大部分情況下 WeakReference 也是可以的,但是在一種特殊的情況下 WeakReference 就不可以了,只能由 PhantomReference 來跟蹤物件的回收狀態。

image

上圖中,object1 物件在 JVM 堆中被一個 WeakReference 物件和 FinalReference 物件同時引用,除此之外沒有任何強引用鏈和軟引用鏈,根據 FinalReference 的語義,這個 object1 是不是就要被回收了,但為了執行它的 finalize() 方法所以 JVM 會將 object1 復活。

根據 WeakReference 的語義,此時發生了 GC,並且 object1 沒有任何強引用鏈和軟引用鏈,那麼此時 JVM 是不是就會將 WeakReference 加入到 _reference_pending_list 中,後面再由 ReferenceHandler 執行緒轉移到 ReferenceQueue 中,等待應用程式的處理。

也就是說在這種情況下,FinalReference 和 WeakReference 在本輪 GC 中,都會被 JVM 處理,但是 object1 卻是存活狀態,所以 WeakReference 不能跟蹤物件的垃圾回收狀態。

image

object2 物件在 JVM 堆中被一個 PhantomReference 物件和 FinalReference 物件同時引用,除此之外沒有任何強引用鏈和軟引用鏈,根據 FinalReference 的語義, JVM 會將 object2 復活。

但根據 PhantomReference 的語義,只有在 object2 要被垃圾回收的時候,JVM 才會將 PhantomReference 加入到 _reference_pending_list 中,但是此時 object2 已經復活了,所以 PhantomReference 這裡就不會被加入到 _reference_pending_list 中了。

也就是說在這種情況下,只有 FinalReference 在本輪 GC 中才會被 JVM 處理,隨後 FinalizerThread 會呼叫 Finalizer 物件(FinalReference型別)的 runFinalizer 方法,最終就會執行到 object2 物件的 finalize() 方法。

當 object2 物件的 finalize() 方法被執行完之後,在下一輪 GC 中就會回收 object2 物件,那麼根據 PhantomReference 的語義,PhantomReference 物件只有在下一輪 GC 中才會被 JVM 加入到 _reference_pending_list 中,隨後被 ReferenceHandler 執行緒處理。

所以在這種特殊的情況就只有 PhantomReference 才能用於跟蹤物件的垃圾回收狀態,而 WeakReference 卻不可以。

那 JVM 是如何實現 PhantomReference 和 WeakReference 的這兩種語義的呢

image

首先在 ZGC 的 Concurrent Mark 階段,GC 執行緒會將 JVM 堆中所有需要被處理的 Reference 物件加入到一個臨時的 _discovered_list 中。

隨後在 Concurrent Process Non-Strong References 階段,GC 會透過 should_drop 方法再次判斷 _discovered_list 中存放的這些臨時 Reference 物件所引用的 referent 是否存活 ?

如果這些 referent 仍然存活,那麼就需要將對應的 Reference 物件從 _discovered_list 中移除。

如果這些 referent 不再存活,那麼就將對應的 Reference 物件繼續保留在 _discovered_list,最後將 _discovered_list 中的 Reference 物件全部轉移到 _reference_pending_list 中,隨後喚醒 ReferenceHandler 執行緒去處理。

PhantomReference 和 WeakReference 的核心區別就在這個 should_drop 方法中:

bool ZReferenceProcessor::should_drop(oop reference, ReferenceType type) const {
  // 獲取 Reference 所引用的 referent
  const oop referent = reference_referent(reference);
  
  // 如果 referent 仍然存活,那麼就會將 Reference 物件移除,不需要被 ReferenceHandler 執行緒處理
  if (type == REF_PHANTOM) {
    // 針對 PhantomReference 物件的特殊處理
    return ZBarrier::is_alive_barrier_on_phantom_oop(referent);
  } else {
    // 針對 WeakReference 物件的處理
    return ZBarrier::is_alive_barrier_on_weak_oop(referent);
  }
}

should_drop 方法主要是用來判斷一個被 Reference 引用的 referent 物件是否存活,但是根據 Reference 型別的不同,比如這裡的 PhantomReference 和 WeakReference,具體的判斷邏輯是不一樣的。

根據前面幾個小節的內容,我們知道 ZGC 是透過一個 _livemap 標記點陣圖,來標記一個物件的存活狀態的,ZGC 會將整個 JVM 堆劃分成一個一個的 page,然後從 page 中一個一個的分配物件。每一個 page 結構中有一個 _livemap,用來標記該 page 中所有物件的存活狀態。

class ZPage : public CHeapObj<mtGC> {
private:
  ZLiveMap           _livemap;
}

在 ZGC 中 ZPage 共分為三種型別:

// Page types
const uint8_t     ZPageTypeSmall                = 0;
const uint8_t     ZPageTypeMedium               = 1;
const uint8_t     ZPageTypeLarge                = 2;
  • ZPageTypeSmall 尺寸為 2M , SmallZPage 中的物件尺寸按照 8 位元組對齊,最大允許的物件尺寸為 256K。

  • ZPageTypeMedium 尺寸和 MaxHeapSize 有關,一般會設定為 32 M,MediumZPage 中的物件尺寸按照 4K 對齊,最大允許的物件尺寸為 4M。

  • ZPageTypeLarge 尺寸不定,但需要按照 2M 對齊。如果一個物件的尺寸超過 4M 就需要在 LargeZPage 中分配。

uintptr_t ZObjectAllocator::alloc_object(size_t size, ZAllocationFlags flags) {
  if (size <= ZObjectSizeLimitSmall) {
    // 物件 size 小於等於 256K ,在 SmallZPage 中分配
    return alloc_small_object(size, flags);
  } else if (size <= ZObjectSizeLimitMedium) {
    // 物件 size 大於 256K 但小於等於 4M ,在 MediumZPage 中分配
    return alloc_medium_object(size, flags);
  } else {
    // 物件 size 超過 4M ,在 LargeZPage 中分配
    return alloc_large_object(size, flags);
  }
}

那麼 ZPage 中的這個 _livemap 中的 bit 位個數,是不是就應該和一個 ZPage 所能容納的最大物件個數保持一致,因為一個物件是否存活按理說是不是用一個 bit 就可以表示了 ?

  • ZPageTypeSmall 中最大能容納的物件個數為 2M / 8B = 262144,那麼對應的 _livemap 中是不是隻要 262144 個 bit 就可以了。

  • ZPageTypeMedium 中最大能容納的物件個數為 32M / 4K = 8192,那麼對應的 _livemap 中是不是隻要 8192 個 bit 就可以了。

  • ZPageTypeLarge 只會容納一個大物件。在 ZGC 中超過 4M 的就是大物件。

inline uint32_t ZPage::object_max_count() const {
  switch (type()) {
  case ZPageTypeLarge:
    // A large page can only contain a single
    // object aligned to the start of the page.
    return 1;

  default:
    return (uint32_t)(size() >> object_alignment_shift());
  }
}

但實際上 ZGC 中的 _livemap 所包含的 bit 個數是在此基礎上再乘以 2,也就是說一個物件需要用兩個 bit 位來標記。

static size_t bitmap_size(uint32_t size, size_t nsegments) {
  return MAX2<size_t>(size, nsegments) * 2;
}

那 ZGC 為什麼要用兩個 bit 來標記物件的存活狀態呢 ?答案就是為了區分本小節中介紹的這種特殊情況,一個物件是否存活分為兩種情況:

  1. 物件被 FinalReference 復活,這樣 ZGC 會標記第一個低位 bit —— 1

  2. 物件存在強引用鏈,人家原本就應該存活,這樣 ZGC 會將兩個 bit 位全部標記 —— 11

而在本小節中我們討論的就是物件在被 FinalReference 復活的情況下,PhantomReference 和 WeakReference 的處理有何不同,瞭解了這些背景知識之後,那麼我們再回頭來看 should_drop 方法的判斷邏輯:

首先對於 PhantomReference 來說,在 ZGC 的 Concurrent Process Non-Strong References 階段是透過 ZBarrier::is_alive_barrier_on_phantom_oop 來判斷其引用的 referent 物件是否存活的。

inline bool ZHeap::is_object_live(uintptr_t addr) const {
  ZPage* page = _page_table.get(addr);
  // PhantomReference 判斷的是第一個低位 bit 是否被標記
  // 而 FinalReference 復活 referent 物件標記的也是第一個 bit 位
  return page->is_object_live(addr);
}

inline bool ZPage::is_object_marked(uintptr_t addr) const {
  //  獲取第一個 bit 位 index
  const size_t index = ((ZAddress::offset(addr) - start()) >> object_alignment_shift()) * 2;
  // 檢視是否被 FinalReference 標記過
  return _livemap.get(index);
}

我們看到 PhantomReference 判斷的是第一個 bit 位是否被標記過,而在 FinalReference 復活 referent 物件的時候標記的就是第一個 bit 位。所以 should_drop 方法返回 true,PhantomReference 從 _discovered_list 中移除。

而對於 WeakReference 來說,卻是透過 Barrier::is_alive_barrier_on_weak_oop 來判斷其引用的 referent 物件是否存活的。

inline bool ZHeap::is_object_strongly_live(uintptr_t addr) const {
  ZPage* page = _page_table.get(addr);
  // WeakReference 判斷的是第二個高位 bit 是否被標記
  return page->is_object_strongly_live(addr);
}

inline bool ZPage::is_object_strongly_marked(uintptr_t addr) const {

  const size_t index = ((ZAddress::offset(addr) - start()) >> object_alignment_shift()) * 2;
  //  獲取第二個 bit 位 index
  return _livemap.get(index + 1);
}

我們看到 WeakReference 判斷的是第二個高位 bit 是否被標記過,所以這種情況下,無論 referent 物件是否被 FinalReference 復活,should_drop 方法都會返回 false 。WeakReference 仍然會保留在 _discovered_list 中,隨後和 FinalReference 一起被 ReferenceHandler 執行緒處理。

所以總結一下他們的核心區別就是:

  1. PhantomReference 物件只有在物件被回收的時候,才會被 ReferenceHandler 執行緒處理,它會被 FinalReference 影響。

  2. WeakReference 物件只要是發生 GC , 並且它引用的 referent 物件沒有任何強引用鏈或者軟引用鏈的時候,都會被 ReferenceHandler 執行緒處理,不會被 FinalReference 影響。

相關文章