OpenJDK17-JVM原始碼閱讀-ZGC-併發標記

京東雲開發者發表於2023-10-11

1、ZGC簡介

1.1 介紹

ZGC 是一款低延遲的垃圾回收器,是 Java 垃圾收集技術的最前沿,理解了 ZGC,那麼便可以說理解了 java 最前沿的垃圾收集技術。

從 JDK11 中作為試驗特性推出以來,ZGC 一直在不停地發展中。

從 JDK14 開始,ZGC 開始支援 Windows。

在 JDK15 中,ZGC 不再是實驗功能,可以正式投入生產使用了。

在最新的 JDK 開源庫中,已經出現了分代收集的 ZGC 程式碼,預計不久的將來會正式釋出,到時相信 ZGC 各項表現將會更加優秀。

圖1 分代收集的ZGC

圖1 分代收集的ZGC

如上圖,JDK21 中已經有了分代 ZGC 的 Feature。

1.2 ZGC 特徵

  1. 低延遲

  2. 大容量堆

  3. 染色指標

  4. 讀屏障

1.3 垃圾收集階段

圖2 ZGC運作過程

圖2 ZGC 運作過程

如上圖,主要有以下幾個階段,初始標記、併發標記/併發重對映、併發預備重分配、初始重分配、併發重分配,本次主要分析的就是”併發標記/併發重對映“部分原始碼。

1.4 帶著問題去讀原始碼

  1. ZGC 是如何在指標上標記的

  2. ZGC 三色標記的過程

  3. ZGC 只用了讀屏障是如何防止漏標的

  4. ZGC 標記過程中的指標自愈過程

  5. ZGC 併發標記中的併發重對映過程

2、原始碼

2.1 入口

整個 ZGC 的原始碼入口是 ZDriver::gc 函式

void ZDriver::gc(const ZDriverRequest& request) {
  ZDriverGCScope scope(request);

  // Phase 1: Pause Mark Start
  pause_mark_start();

  // Phase 2: Concurrent Mark
  concurrent(mark);

  // Phase 3: Pause Mark End
  while (!pause_mark_end()) {
    // Phase 3.5: Concurrent Mark Continue
    concurrent(mark_continue);
  }

  // Phase 4: Concurrent Mark Free
  concurrent(mark_free);

  // Phase 5: Concurrent Process Non-Strong References
  concurrent(process_non_strong_references);

  // Phase 6: Concurrent Reset Relocation Set
  concurrent(reset_relocation_set);

  // Phase 7: Pause Verify
  pause_verify();

  // Phase 8: Concurrent Select Relocation Set
  concurrent(select_relocation_set);

  // Phase 9: Pause Relocate Start
  pause_relocate_start();

  // Phase 10: Concurrent Relocate
  concurrent(relocate);
}

其中,concurrent() 是一個宏定義

// Macro to execute a termination check after a concurrent phase. Note
// that it's important that the termination check comes after the call
// to the function f, since we can't abort between pause_relocate_start()
// and concurrent_relocate(). We need to let concurrent_relocate() call
// abort_page() on the remaining entries in the relocation set.
#define concurrent(f)                 \
  do {                                \
    concurrent_##f();                 \
    if (should_terminate()) {         \
      return;                         \
    }                                 \
  } while (false)

於是我們可以知道併發標記的函式是 concurrent_mark

void ZDriver::concurrent_mark() {
  ZStatTimer timer(ZPhaseConcurrentMark);
  ZBreakpoint::at_after_marking_started();
  //開始對整個堆進行標記
  ZHeap::heap()->mark(true /* initial */);
  ZBreakpoint::at_before_marking_completed();
}

2.2 併發標記過程

2.2.1 併發標記

接下來看 ZHeap::heap()->mark

void ZHeap::mark(bool initial) {
  _mark.mark(initial);
}

再往下

void ZMark::mark(bool initial) {
  if (initial) {
    ZMarkRootsTask task(this);
    _workers->run(&task);
  }

  ZMarkTask task(this);
  _workers->run(&task);
}

這裡用了一個任務框架,執行任務邏輯在 ZMarkTask 裡

class ZMarkTask : public ZTask {
private:
  ZMark* const   _mark;
  const uint64_t _timeout_in_micros;

public:
  ZMarkTask(ZMark* mark, uint64_t timeout_in_micros = 0) :
      ZTask("ZMarkTask"),
      _mark(mark),
      _timeout_in_micros(timeout_in_micros) {
    _mark->prepare_work();
  }

  ~ZMarkTask() {
    _mark->finish_work();
  }

  virtual void work() {
    _mark->work(_timeout_in_micros);
  }
};

這裡具體執行的函式是 work,往下看 _mark->work

void ZMark::work(uint64_t timeout_in_micros) {
  ZMarkCache cache(_stripes.nstripes());
  ZMarkStripe* const stripe = _stripes.stripe_for_worker(_nworkers, ZThread::worker_id());
  ZMarkThreadLocalStacks* const stacks = ZThreadLocalData::stacks(Thread::current());

  if (timeout_in_micros == 0) {
    work_without_timeout(&cache, stripe, stacks);
  } else {
    work_with_timeout(&cache, stripe, stacks, timeout_in_micros);
  }

  // Flush and publish stacks
  stacks->flush(&_allocator, &_stripes);

  // Free remaining stacks
  stacks->free(&_allocator);
}

這裡的 stripe 就是標記條帶,stacks 是一個執行緒本地棧。

往下看 work_with_timeout 方法

void ZMark::work_with_timeout(ZMarkCache* cache, ZMarkStripe* stripe, ZMarkThreadLocalStacks* stacks, uint64_t timeout_in_micros) {
  ZStatTimer timer(ZSubPhaseMarkTryComplete);
  ZMarkTimeout timeout(timeout_in_micros);

  for (;;) {
    if (!drain(stripe, stacks, cache, &timeout)) {
      // Timed out
      break;
    }

    if (try_steal(stripe, stacks)) {
      // Stole work
      continue;
    }

    // Terminate
    break;
  }
}

可以看到這裡是一個迴圈,會一直從標記條帶裡取資料,直到取完了或時間到了。 這裡就是 ZGC 三色標記的主迴圈了。 接下來看 drain 函式,drain 就是使排空的意思,這裡就是從棧中取出指標進行標記,直到棧為空。

template <typename T>
bool ZMark::drain(ZMarkStripe* stripe, ZMarkThreadLocalStacks* stacks, ZMarkCache* cache, T* timeout) {
  ZMarkStackEntry entry;

  // Drain stripe stacks
  while (stacks->pop(&_allocator, &_stripes, stripe, entry)) {
    //標記和跟隨
    mark_and_follow(cache, entry);

    // Check timeout
    if (timeout->has_expired()) {
      // Timeout
      return false;
    }
  }

  // Success
  return !timeout->has_expired();
}

可以看到這裡一直在從棧裡取資料,然後標記和遞迴標記,直到棧排空。

既然有從棧裡取資料的地方,那肯定有往棧裡放資料的地方,我們先把這個點記下來,叫“記錄點1”。

接著往下看 mark_and_follow 函式

void ZMark::mark_and_follow(ZMarkCache* cache, ZMarkStackEntry entry) {
  // Decode flags
  const bool finalizable = entry.finalizable();
  const bool partial_array = entry.partial_array();

  if (partial_array) {
    follow_partial_array(entry, finalizable);
    return;
  }

  // Decode object address and additional flags
  const uintptr_t addr = entry.object_address();
  const bool mark = entry.mark();
  bool inc_live = entry.inc_live();
  const bool follow = entry.follow();

  ZPage* const page = _page_table->get(addr);
  assert(page->is_relocatable(), "Invalid page state");

  // Mark
  //標記物件
  if (mark && !page->mark_object(addr, finalizable, inc_live)) {
    // Already marked
    return;
  }

  // Increment live
  if (inc_live) {
    // Update live objects/bytes for page. We use the aligned object
    // size since that is the actual number of bytes used on the page
    // and alignment paddings can never be reclaimed.
    const size_t size = ZUtils::object_size(addr);
    const size_t aligned_size = align_up(size, page->object_alignment());
    cache->inc_live(page, aligned_size);
  }

  // Follow
  if (follow) {
    if (is_array(addr)) {
      //遞迴處理陣列
      follow_array_object(objArrayOop(ZOop::from_address(addr)), finalizable);
    } else {
      //遞迴處理物件
      follow_object(ZOop::from_address(addr), finalizable);
    }
  }
}

這裡有兩個函式要看,我們先看 page->mark_object,再看 follow_object,至於follow_array_object,和 follow_object 類似,就不在詳細介紹了。

page->mark_object 原始碼如下

inline bool ZPage::mark_object(uintptr_t addr, bool finalizable, bool& inc_live) {
  assert(ZAddress::is_marked(addr), "Invalid address");
  assert(is_relocatable(), "Invalid page state");
  assert(is_in(addr), "Invalid address");

  // Set mark bit
  const size_t index = ((ZAddress::offset(addr) - start()) >> object_alignment_shift()) * 2;
  return _livemap.set(index, finalizable, inc_live);
}

可以看到這裡用了一個 map,來儲存了所有物件地址的 finalizable 和 inc_live(新增存活)資訊。

const size_t index = ((ZAddress::offset(addr) - start()) >> object_alignment_shift()) * 2;

從這個程式碼看,livemap 的大小大概是整個堆的物件對齊大小分之一,然後再乘以2。 這個 map 存的資訊非常重要,ZGC中判斷一個地址是否被標記過,一個物件是否存活都是透過這個 map 裡的資訊來判斷的。

再回到上一步,繼續看 follow_object 函式

void ZMark::follow_object(oop obj, bool finalizable) {
  if (finalizable) {
    ZMarkBarrierOopClosure<true /* finalizable */> cl;
    obj->oop_iterate(&cl);
  } else {
    ZMarkBarrierOopClosure<false /* finalizable */> cl;
    obj->oop_iterate(&cl);
  }
}

這裡根據 finalizable 分了兩個分支,oop_iterate 看名字我們也能猜到這是做什麼的,就是對物件裡的指標進行迭代,但畢竟是猜測,具體還要看程式碼驗證。

ZMarkBarrierOopClosure 這個看名字就是讀屏障了,我們已經來到開頭問題 3 的大門前了,只差臨門一腳。 踹進讀屏障的大門前,我們還得搬開擋在門口的一個大石頭 oop_iterate 方法是怎麼和 ZMarkBarrierOopClosure 聯絡上的。

2.2.2 物件迭代遍歷

這一節將看 oop_iterate 內部邏輯,這個屬於分支流程,暫不想了解的可以先跳過,直接看下一節 2.2.3 讀屏障。

警告:抓緊扶手,下面車速開始加快了!

先把 cl 型別 ZMarkBarrierOopClosure 記錄下來,我們記為 “記錄點2”。

進入 oop_iterate 函式

template <typename OopClosureType>
void oopDesc::oop_iterate(OopClosureType* cl) {
  OopIteratorClosureDispatch::oop_oop_iterate(cl, this, klass());
}

再往下

template <typename OopClosureType>
void OopIteratorClosureDispatch::oop_oop_iterate(OopClosureType* cl, oop obj, Klass* klass, MemRegion mr) {
  OopOopIterateBoundedDispatch<OopClosureType>::function(klass)(cl, obj, klass, mr);
}

再往下

static FunctionType function(Klass* klass) {
    return _table._function[klass->id()];
  }

function 方法在一個類中,_table 就是這個類的物件例項,_function 是個陣列,我們看_function 怎麼初始化值的。

public:
    FunctionType _function[KLASS_ID_COUNT];

    Table(){
      set_init_function<InstanceKlass>();
      set_init_function<InstanceRefKlass>();
      set_init_function<InstanceMirrorKlass>();
      set_init_function<InstanceClassLoaderKlass>();
      set_init_function<ObjArrayKlass>();
      set_init_function<TypeArrayKlass>();
    }

table 構造方法與 _function 定義

這裡有多個 Klass 物件,我們只看第一個。我們把 InstanceKlass 暫記為“記錄點3”,接著往下看 set_init_function。

template <typename KlassType>
    void set_init_function() {
      _function[KlassType::ID] = &init<KlassType>;
    }

往下看 init 函式

template <typename KlassType>
    static void init(OopClosureType* cl, oop obj, Klass* k, MemRegion mr) {
      OopOopIterateBoundedDispatch<OopClosureType>::_table.set_resolve_function_and_execute<KlassType>(cl, obj, k, mr);
    }

繼續

template <typename KlassType>
    void set_resolve_function_and_execute(OopClosureType* cl, oop obj, Klass* k, MemRegion mr) {
      set_resolve_function<KlassType>();
      _function[KlassType::ID](cl, obj, k, mr);
    }

繼續跟 set_resolve_function

template <typename KlassType>
    void set_resolve_function() {
      if (UseCompressedOops) {
        _function[KlassType::ID] = &oop_oop_iterate_bounded<KlassType, narrowOop>;
      } else {
        _function[KlassType::ID] = &oop_oop_iterate_bounded<KlassType, oop>;
      }
    }

可以看到這裡有個 UseCompressedOops,難道 ZGC 支援壓縮指標了?感興趣的同學可以深入研究下。

往下看 oop_oop_iterate_bounded

template <typename KlassType, typename T>
    static void oop_oop_iterate_bounded(OopClosureType* cl, oop obj, Klass* k, MemRegion mr) {
      ((KlassType*)k)->KlassType::template oop_oop_iterate_bounded<T>(obj, cl, mr);
    }

這裡可以看到呼叫了 KlassType 的 oop_oop_iterate_bounded 函式,KlassType 又是啥,這是一個泛型,其真實型別在這裡其中之一就是記錄點3的 InstanceKlass。

在 JVM 中一個普通的 Java 類是以一個 InstanceKlass 型別的 Klass 模型存在的。

以下就不屬於 ZGC 的原始碼了,當然,仍然是 jvm 原始碼。

進入 InstanceKlass 的 oop_oop_iterate_bounded 函式

template <typename T, class OopClosureType>
ALWAYSINLINE void InstanceKlass::oop_oop_iterate_bounded(oop obj, OopClosureType* closure, MemRegion mr) {
  if (Devirtualizer::do_metadata(closure)) {
    if (mr.contains(obj)) {
      Devirtualizer::do_klass(closure, this);
    }
  }

  oop_oop_iterate_oop_maps_bounded<T>(obj, closure, mr);
}

往下

template <typename T, class OopClosureType>
ALWAYSINLINE void InstanceKlass::oop_oop_iterate_oop_maps_bounded(oop obj, OopClosureType* closure, MemRegion mr) {
  //非靜態成員變數塊起始位置
  OopMapBlock* map           = start_of_nonstatic_oop_maps();
  //非靜態成員變數塊結束位置
  OopMapBlock* const end_map = map + nonstatic_oop_map_count();
  //對非靜態成員變數塊進行遍歷
  for (;map < end_map; ++map) {
    oop_oop_iterate_oop_map_bounded<T>(map, obj, closure, mr);
  }
}

可以看到這裡對一個物件的非靜態成員變數塊進行了迭代遍歷。 繼續往下

template <typename T, class OopClosureType>
ALWAYSINLINE void InstanceKlass::oop_oop_iterate_oop_map_bounded(OopMapBlock* map, oop obj, OopClosureType* closure, MemRegion mr) {
  T* p   = (T*)obj->obj_field_addr<T>(map->offset());
  T* end = p + map->count();

  T* const l   = (T*)mr.start();
  T* const h   = (T*)mr.end();
  assert(mask_bits((intptr_t)l, sizeof(T)-1) == 0 &&
         mask_bits((intptr_t)h, sizeof(T)-1) == 0,
         "bounded region must be properly aligned");

  if (p < l) {
    p = l;
  }
  if (end > h) {
    end = h;
  }

  for (;p < end; ++p) {
    Devirtualizer::do_oop(closure, p);
  }
}

這裡繼續對一個物件的非靜態成員變數進行遍歷。 繼續看 Devirtualizer::do_oop

template <typename OopClosureType, typename T>
inline void Devirtualizer::do_oop(OopClosureType* closure, T* p) {
  call_do_oop<T>(&OopClosureType::do_oop, &OopClosure::do_oop, closure, p);
}

繼續

template <typename T, typename Receiver, typename Base, typename OopClosureType>
static typename EnableIf<IsSame<Receiver, Base>::value, void>::type
call_do_oop(void (Receiver::*)(T*), void (Base::*)(T*), OopClosureType* closure, T* p) {
  closure->do_oop(p);
}

繼續,但繼續不動了,OopClosureType 是個模板型別,其真實型別是什麼呢?一層層往上回溯,發現它就是記錄點2的 ZMarkBarrierOopClosure 讀屏障,這就又回到了 ZGC 主流程裡了。

2.2.3 讀屏障

進入 ZMarkBarrierOopClosure,由上一節可知,這個函式的入參 p 就是要標記物件的非靜態成員變數的指標。

template <bool finalizable>
class ZMarkBarrierOopClosure : public ClaimMetadataVisitingOopIterateClosure {
public:
  ZMarkBarrierOopClosure() :
      ClaimMetadataVisitingOopIterateClosure(finalizable
                                                 ? ClassLoaderData::_claim_finalizable
                                                 : ClassLoaderData::_claim_strong,
                                             finalizable
                                                 ? NULL
                                                 : ZHeap::heap()->reference_discoverer()) {}

  virtual void do_oop(oop* p) {
    ZBarrier::mark_barrier_on_oop_field(p, finalizable);
  }

  virtual void do_oop(narrowOop* p) {
    ShouldNotReachHere();
  }
};

繼續看 mark_barrier_on_oop_field

//
// Mark barrier
//
inline void ZBarrier::mark_barrier_on_oop_field(volatile oop* p, bool finalizable) {
  const oop o = Atomic::load(p);

  if (finalizable) {
    barrier<is_marked_or_null_fast_path, mark_barrier_on_finalizable_oop_slow_path>(p, o);
  } else {
    const uintptr_t addr = ZOop::to_address(o);
    if (ZAddress::is_good(addr)) {
      // Mark through good oop
      mark_barrier_on_oop_slow_path(addr);
    } else {
      // Mark through bad oop
      barrier<is_good_or_null_fast_path, mark_barrier_on_oop_slow_path>(p, o);
    }
  }
}

這裡分了3個分支,finalizable先不看,根據指標是good還是bad有分了兩個分支,這兩個分支最終有都呼叫了 mark_barrier_on_oop_slow_path 慢路徑處理方法。

這裡直接看下面的第 2 個分支

template <ZBarrierFastPath fast_path, ZBarrierSlowPath slow_path>
inline oop ZBarrier::barrier(volatile oop* p, oop o) {
  const uintptr_t addr = ZOop::to_address(o);

  // Fast path
  if (fast_path(addr)) {
    return ZOop::from_address(addr);
  }

  // Slow path
  const uintptr_t good_addr = slow_path(addr);

  if (p != NULL) {
    //指標自愈
    self_heal<fast_path>(p, addr, good_addr);
  }

  return ZOop::from_address(good_addr);
}

這是讀屏障的核心邏輯程式碼,這裡我們也看到了指標自愈。

從這段程式碼可以看到,首先執行快速路徑檢查,如果 fast_path(addr) 返回 true,則直接返回 addr 所對應的 oop 物件。 如果快速路徑檢查失敗,則執行慢速路徑檢查,將 addr 傳遞給 slow_path 函式,並將返回值儲存在 good_addr 變數中。 如果傳入的指標 p 不為空,則呼叫 self_heal

這裡的 slow_path 其實就是 mark_barrier_on_oop_slow_path 函式。

這裡主要往下看兩個函式,慢路徑處理和指標自愈,慢路徑處理流程比較長,我們先看指標自愈。

template <ZBarrierFastPath fast_path>
inline void ZBarrier::self_heal(volatile oop* p, uintptr_t addr, uintptr_t heal_addr) {
  if (heal_addr == 0) {
    // Never heal with null since it interacts badly with reference processing.
    // A mutator clearing an oop would be similar to calling Reference.clear(),
    // which would make the reference non-discoverable or silently dropped
    // by the reference processor.
    return;
  }

  assert(!fast_path(addr), "Invalid self heal");
  assert(fast_path(heal_addr), "Invalid self heal");

  for (;;) {
    // Heal
    const uintptr_t prev_addr = Atomic::cmpxchg((volatile uintptr_t*)p, addr, heal_addr);
    if (prev_addr == addr) {
      // Success
      return;
    }

    if (fast_path(prev_addr)) {
      // Must not self heal
      return;
    }

    // The oop location was healed by another barrier, but still needs upgrading.
    // Re-apply healing to make sure the oop is not left with weaker (remapped or
    // finalizable) metadata bits than what this barrier tried to apply.
    assert(ZAddress::offset(prev_addr) == ZAddress::offset(heal_addr), "Invalid offset");
    addr = prev_addr;
  }
}

這個就比較簡單了,類似 Java 的 cas 操作,把壞指標修改為好指標,然後執行快速路徑。

好指標是上面執行慢路徑返回的。

接下來看慢路徑

//
// Mark barrier
//
uintptr_t ZBarrier::mark_barrier_on_oop_slow_path(uintptr_t addr) {
  assert(during_mark(), "Invalid phase");
  assert(ZThread::is_worker(), "Invalid thread");

  // Mark
  return mark<GCThread, Follow, Strong, Overflow>(addr);
}

這裡對進入讀屏障的指標又進行了標記。

到這裡,其實讀屏障主要邏輯已經結束了,下面是讀屏障觸發的標記邏輯。

2.2.4 讀屏障觸發的標記邏輯

繼續

template <bool gc_thread, bool follow, bool finalizable, bool publish>
uintptr_t ZBarrier::mark(uintptr_t addr) {
  uintptr_t good_addr;

  if (ZAddress::is_marked(addr)) {
    // Already marked, but try to mark though anyway
    good_addr = ZAddress::good(addr);
  } else if (ZAddress::is_remapped(addr)) {
    // Already remapped, but also needs to be marked
    good_addr = ZAddress::good(addr);
  } else {
    // Needs to be both remapped and marked
    good_addr = remap(addr);
  }

  // Mark
  if (should_mark_through<finalizable>(addr)) {
    ZHeap::heap()->mark_object<gc_thread, follow, finalizable, publish>(good_addr);
  }

  if (finalizable) {
    // Make the oop finalizable marked/good, instead of normal marked/good.
    // This is needed because an object might first becomes finalizable
    // marked by the GC, and then loaded by a mutator thread. In this case,
    // the mutator thread must be able to tell that the object needs to be
    // strongly marked. The finalizable bit in the oop exists to make sure
    // that a load of a finalizable marked oop will fall into the barrier
    // slow path so that we can mark the object as strongly reachable.
    return ZAddress::finalizable_good(good_addr);
  }

  return good_addr;
}

這裡要繼續跟的函式有三個 ZAddress::good 、remap、ZHeap::heap()->mark_object。

ZHeap::heap()->mark_object 就是在堆中對物件進行標記。

先看下 ZAddress::good 的邏輯

inline uintptr_t ZAddress::offset(uintptr_t value) {
  return value & ZAddressOffsetMask;
}

inline uintptr_t ZAddress::good(uintptr_t value) {
  return offset(value) | ZAddressGoodMask;
}

可以看到只是改變了下指標標記位,比較簡單,也符合我們沒讀程式碼前對 ZGC 流程的認識。

2.2.5 重對映

繼續看 remap 重對映方法,這裡我們就找到了開頭問題5【ZGC併發標記中的併發重對映過程】

uintptr_t ZBarrier::remap(uintptr_t addr) {
  assert(!ZAddress::is_good(addr), "Should not be good");
  assert(!ZAddress::is_weak_good(addr), "Should not be weak good");
  return ZHeap::heap()->remap_object(addr);
}

繼續

inline uintptr_t ZHeap::relocate_object(uintptr_t addr) {
  assert(ZGlobalPhase == ZPhaseRelocate, "Relocate not allowed");

  ZForwarding* const forwarding = _forwarding_table.get(addr);
  if (forwarding == NULL) {
    // Not forwarding
    return ZAddress::good(addr);
  }

  // Relocate object
  return _relocate.relocate_object(forwarding, ZAddress::good(addr));
}

可以看到這裡先查轉發表,轉發表中不存在直接返回好指標,存在則執行重分配操作,再往下就是重分配的邏輯了,本文中就不詳解了,在以後重分配文章中的詳細介紹。

2.2.6 回到讀屏障標記流程

然後,返回 2.2.4 開頭,繼續看 ZHeap::heap()->mark_object

template <bool gc_thread, bool follow, bool finalizable, bool publish>
inline void ZHeap::mark_object(uintptr_t addr) {
  assert(ZGlobalPhase == ZPhaseMark, "Mark not allowed");
  _mark.mark_object<gc_thread, follow, finalizable, publish>(addr);
}

繼續

template <bool gc_thread, bool follow, bool finalizable, bool publish>
inline void ZMark::mark_object(uintptr_t addr) {
  assert(ZAddress::is_marked(addr), "Should be marked");

  ZPage* const page = _page_table->get(addr);
  if (page->is_allocating()) {
    // Already implicitly marked
    return;
  }

  const bool mark_before_push = gc_thread;
  bool inc_live = false;

  if (mark_before_push) {
    // Try mark object
    if (!page->mark_object(addr, finalizable, inc_live)) {
      // Already marked
      return;
    }
  } else {
    // Don't push if already marked
    if (page->is_object_marked<finalizable>(addr)) {
      // Already marked
      return;
    }
  }

  // Push
  ZMarkThreadLocalStacks* const stacks = ZThreadLocalData::stacks(Thread::current());
  ZMarkStripe* const stripe = _stripes.stripe_for_addr(addr);
  ZMarkStackEntry entry(addr, !mark_before_push, inc_live, follow, finalizable);
  stacks->push(&_allocator, &_stripes, stripe, entry, publish);
}

這裡有兩個重要邏輯,一是已經標記的不再進行 push,二是未標記過的 push 到棧裡。

還記得前面的“記錄點1”嗎,push 指標 到棧裡的地方就是這兒了,被 push 的指標將會在下次標記迴圈中被取出來。

到此,ZGC 三色標記的主流程就頭尾相接了,完結撒花。

3、問題回顧

3.1 開頭的問題

  1. ZGC是如何在指標上標記的

  2. ZGC三色標記的過程

  3. ZGC只用了讀屏障是如何防止漏標的

  4. ZGC標記過程中的指標自愈過程

  5. ZGC併發標記中的併發重對映過程

上面5個問題,1、2、4、5上面原始碼分析過程中都已經有了,問題3是要從整個流程上看。

3.2 ZGC只用了讀屏障是如何防止漏標的

3.2.1 什麼情況下會漏標

當以下兩個情況同時發生時:

  1. 新增了一個從黑色物件到白色物件的新引用

  2. 刪除了全部從灰色物件到該白色物件的引用

3.2.2 如何避免?

破壞條件1,這就是增量更新(Incremental Update)解決方案。

破壞條件2,這就是原始快照(Snapshot At The Beginning, SAT B )解決方案。

ZGC 採用的是增量更新解決方案。但是,增量更新不是新增嗎,應該是寫屏障啊,讀屏障怎麼做到呢?

其實新增一個引用時,之前肯定要從某處讀到這個引用,那麼這個讀的操作便觸發了讀屏障。前面原始碼中,我們知道 ZGC 讀屏障裡有一個標記的過程,而且還會自動返回好指標,也就是說經過讀屏障返回的指標中不存在白色物件,這就從根本上避免了條件1。

4、擴充套件思考

ZGC 標記是在指標上,而不是在物件上,當回收某個 region 時,對於此 region 裡的某個物件,無法獲得其他物件指向此物件的指標,那如何得知這物件是否存活呢?

這個問題需要我們將標記階段和重分配階段的程式碼連起來看才能得到答案了。

5、結束

對於ZGC併發標記的過程的原始碼分析就到這裡了,如果覺得對您有啟發或有幫助就多多點贊支援呀~

作者:京東物流 劉家存

來源:京東雲開發者社群 自猿其說Tech 轉載請註明來源

相關文章