從源碼出發看zgc的技術內幕

        筆者通過上次對zgc在不一樣環境下進行的測試後,發現zgc所帶來的提高很是之大。一時間對zgc在生產中使用充滿信心,可是在全面使用以前,不免對其幾大新特性有一些好奇,好比:染色指針,讀屏障,動態region,支持NUMA等等。其中有一些是比較好理解的,可是有一些例如染色指針,讀屏障剛接觸的時候會不明其意。在網上搜索一番後發現不少文檔都只是簡單一筆蓋過,或者只介紹個概念,甚至還有錯誤或者模糊的介紹,具體的實現和意義最讓筆者好奇,但又找不到答案。因此筆者在通過一段時間的ZGC源碼學習後,在此作一番總結。數組

        特性一:染色指針數據結構

        咱們都知道不論哪一個垃圾回收器都須要對對象進行標記,只有標記過的對象纔是存活的對象,未被標記的對象將在GC中被回收掉。zgc的對象標記實現用的則是染色指針技術。(傳統的GC都是將標記記錄在對象頭中,G1則是將標記記錄在與對象獨立的數據結構上-----Rset)閉包

       話很少說先看一張圖:併發

       從圖中咱們能夠得知zgc在64位操做系統的虛擬地址中取了4個bit進行標記,分別是app

       43---Marked0,44---Marked1,45---Remapped,46---Finalizeble(這裏能夠先簡單瞭解,後面會具體講不一樣標誌位的意義)異步

       前42位(jdk15中jvm會根據堆設置大小控制長度42-44位)則是具體的對象地址,後18位無心義。jvm

       在GC過程當中會對對象的指針進行標記,這樣就能夠在GC標記追蹤對象的時候只與指針打交道,而不用管對象自己。函數

        以上就是在網上或者其餘文檔中能夠搜索到的關於染色指針的解讀,筆者瞭解下來仍舊對其很模糊,因此下面咱們看下源碼更深入的理解染色指針技術的意義。工具

        1.1 染色指針源碼解讀(基於openjdk15):oop

        咱們先看下建立對象返回指針的方法:

        這裏的ZAddress能夠理解爲用來處理染色指針的工具類,good()方法是返回對象在當前視圖的好指針的方法。看到這裏會出現兩個問題:

                1.視圖:即marked0, marked1,remapped視圖,他們對應用一個物理空間,在ZGC中這三個視圖在同一時間點有且僅有一個生效。每一個視圖是一個對物理地址的映射。

                2.好指針:簡單理解就是當前視圖下的指針在當前視圖下是好指針,在其餘視圖下即爲壞指針(先簡單理解) 

//例:建立大對象
uintptr_t ZObjectAllocator::alloc_large_object(size_t size, ZAllocationFlags flags) {
  uintptr_t addr = 0;
  const size_t page_size = align_up(size, ZGranuleSize);
//申請一個大頁
  ZPage* const page = alloc_page(ZPageTypeLarge, page_size, flags);
  if (page != NULL) {
//從頁中申請大小
    addr = page->alloc_object(size);
  }
  return addr;
}
//返回當前頁top
inline uintptr_t ZPage::alloc_object(size_t size) {
  const size_t aligned_size = align_up(size, object_alignment());
//top實際上是頁中虛擬內存段上的地址 即0-4T
  const uintptr_t addr = top();
  const uintptr_t new_top = addr + aligned_size;
  if (new_top > end()) {
    return 0;
  }
//修改新top
  _top = new_top;
//返回地址----轉化位當前視圖的good指針
  return ZAddress::good(addr);
}

            看下good()方法:

            這裏能夠看出好指針的意義,好指針會和當前的GoodMask進行或運算,GoodMask會根據當前視圖切換而改變

//每次建立對象後都會用這個方法處理返回地址
ZAddress::good(addr);
//取先後42位(42位 4T)(44位 16T)地址位
//ZAddressOffsetMask 的前4位染色位是0
//後面全是1,與符號計算後能夠過濾掉前4位
inline uintptr_t ZAddress::offset(uintptr_t value) {
  return value & ZAddressOffsetMask;
}
//用對染色位進行賦值
//ZAddressGoodMasked其實是當前視圖下的標記
//能夠是ZAddressMetadataMarked0
//或ZAddressMetadataMarked1
//或ZAddressMetadataRemapped
//這三個標記只有染色位有1,因此能夠進行指針染色
//這個就是當前視圖下的指針染色方法
inline uintptr_t ZAddress::good(uintptr_t value) {
  return offset(value) | ZAddressGoodMask;
}

        視圖切換方法:

//切換視圖方法,本質是修改good指針判斷標記
//切換到marked視圖,這個方法會從marked0切換到marked1,從marked1切換到marked0
void ZAddress::flip_to_marked() {
  ZAddressMetadataMarked ^= (ZAddressMetadataMarked0 | ZAddressMetadataMarked1);
  set_good_mask(ZAddressMetadataMarked);
}
//切換到remapped視圖
void ZAddress::flip_to_remapped() {
  set_good_mask(ZAddressMetadataRemapped);
}

       返回一個地址對應視圖下地址的方法:

       默認狀況下   ZAddressMetadataMarked0   = 1<<42

                            ZAddressMetadataMarked1   = 1<<43

                             ZAddressMetadataRemapped   = 1<<44

        這裏返回的就是傳入地址的視圖下映射的地址

inline uintptr_t ZAddress::marked0(uintptr_t value) {
  return offset(value) | ZAddressMetadataMarked0;
}
inline uintptr_t ZAddress::marked1(uintptr_t value) {
  return offset(value) | ZAddressMetadataMarked1;
}
inline uintptr_t ZAddress::remapped(uintptr_t value) {
  return offset(value) | ZAddressMetadataRemapped;
}

 

        到這裏代碼是比較好看懂的,可是筆者不禁得又產生一個疑問,咱們如今知道了如何判斷當前視圖,可是視圖是怎麼產生的?

        視圖是由多重內存映射產生的,多重內存映射是使用染色指針技術的產物(可是筆者理解zgc中的染色指針實現方法是多重內存映射)

  

        在看內存映射代碼以前咱們要先了解下jvm關於內存的代碼結構:

        zColloectHeap----zgc堆的集合

        zHeap---zgc堆

        zPageCache---包含一個zPage集合

        zPage---zgc頁(zgc的page概念至關於g1的region),每一個頁內部包含一段物理內存和一段虛擬內存

        zVirtualMemory---虛擬內存,由ZVirtualMemoryManager建立並管理

        zPhysicalMemory---物理內存(這裏實際上仍是用戶空間的虛擬內存,只不過zgc將其管理的虛擬內存分爲物理內存和虛擬內存便於管理,後面的物理內存都是指的是這個)由ZPhysicalMemoryManager建立並管理

        zPhysicalMemorySegment---zgc的物理內存會分爲一個個segment方便管理

        由此咱們能夠找到指針映射的入口:

//入口
jint Universe::initialize_heap() {
  assert(_collectedHeap == NULL, "Heap already created");
  _collectedHeap = GCConfig::arguments()->create_heap();

  return _collectedHeap->initialize();
}

//一切的開始 ZArguments是 GCArguments的子類
CollectedHeap* ZArguments::create_heap() {
  //new 一個zgcCollectedHeap
  return new ZCollectedHeap();
}
//ZCollectedHeap的析構函數(相似構造函數)中會建立一個zHeap
//在初始化zheap的時候調用ZPageAllocator析構函數會先初始化虛擬內存,以後初始化物理內存
//這裏就不貼出具體調用過程

//虛擬內存析構函數
ZVirtualMemoryManager::ZVirtualMemoryManager(size_t max_capacity) :
    _manager(),
    _initialized(false) {

  ...

  //申請地址空間
  if (!reserve(max_capacity)) {
    log_error_pd(gc)("Failed to reserve enough address space for Java heap");
    return;
  }

  ...

}

//最終會調用這個方法,把虛擬地址映射成3個虛擬地址視圖
bool ZVirtualMemoryManager::reserve_contiguous(size_t size) {
  // Allow at most 8192 attempts spread evenly across [0, ZAddressOffsetMax)
  const size_t end = ZAddressOffsetMax - size;
  const size_t increment = align_up(end / 8192, ZGranuleSize);
  for (size_t start = 0; start <= end; start += increment) {
    //這裏把最大地址切分紅2m的段,會選擇從能夠映射的段開始映射,大小爲堆大小
    if (reserve_contiguous_platform(start, size)) {
      // 映射完的內存會加入zMemory的FreeList集合中
      // 虛擬內存映射完的內存會加入zMemory的FreeList集合中
      _manager.free(start, size);
      return true;
    }
  }
  return false;
}

bool ZVirtualMemoryManager::reserve_contiguous_platform(uintptr_t start, size_t size) {
  //獲取地址的三個視圖,這三個方法即是上面提到的
  const uintptr_t marked0 = ZAddress::marked0(start);
  const uintptr_t marked1 = ZAddress::marked1(start);
  const uintptr_t remapped = ZAddress::remapped(start);
  //同一個地址映射三次
  if (!map(marked0, size)) {
    return false;
  }

  if (!map(marked1, size)) {
    unmap(marked0, size);
    return false;
  }

  if (!map(remapped, size)) {
    unmap(marked0, size);
    unmap(marked1, size);
    return false;
  }
  
  ...

  return true;
}
//映射地址的方法
//虛擬內存這裏是匿名映射,這裏本質是分配內存,做用相似malloc,簡單說就是分配一段內存,返回該內存的
//真實地址(這裏是虛擬內存)
static bool map(uintptr_t start, size_t size) {
  const void* const res = mmap((void*)start, size, PROT_NONE, MAP_ANONYMOUS|MAP_PRIVATE|MAP_NORESERVE, -1, 0);
  if (res == MAP_FAILED) {
    return false;
  }
  if ((uintptr_t)res != start) {
    unmap((uintptr_t)res, size);
    return false;
  }
  // Success
  return true;
}

       下面來看zgc管理的物理內存源碼:

//在ZPageAllocator的析構函數中會申請物理內存
//這裏的物理內存並非真正意義上的物理內存。仍然是用戶空間的虛擬內存。
//最終在ZPageAllocator的析構函數中會調用去申請page
bool ZPageAllocator::prime_cache(ZWorkers* workers, size_t size) {
  ZAllocationFlags flags;
  flags.set_non_blocking();
  flags.set_low_address();
  ZPage* const page = alloc_page(ZPageTypeLarge, size, flags);
  if (page == NULL) {
    return false;
  }
  free_page(page, false /* reclaimed */);

  return true;
}
//若是沒有頁會去建立
Page* ZPageAllocator::alloc_page_create(ZPageAllocation* allocation) {
  const size_t size = allocation->size();
  //申請一段虛擬內存加入到頁中,實際上是從上面提到的zMemory的FreeList集合中申請
  const ZVirtualMemory vmem = _virtual.alloc(size, allocation->flags().low_address());

  ....
  // Create new page
  return new ZPage(allocation->type(), vmem, pmem);
}

//最終會調用這個方法,建立的page會進行映射,page->start()返回的是頁中虛擬內存段的start
void ZPageAllocator::map_page(const ZPage* page) const {
  // Map physical memory
  _physical.map(page->start(), page->physical_memory());
}

//這裏會將三個視圖進行映射到同一個物理內存上(能夠是文件,能夠是內存)和文件描述符有關
void ZPhysicalMemoryManager::map(uintptr_t offset, const ZPhysicalMemory& pmem) const {
  const size_t size = pmem.size();

  if (ZVerifyViews) {
    // Map good view
    map_view(ZAddress::good(offset), pmem);
  } else {
    //映射全部視圖
    map_view(ZAddress::marked0(offset), pmem);
    map_view(ZAddress::marked1(offset), pmem);
    map_view(ZAddress::remapped(offset), pmem);
  }

  nmt_commit(offset, size);
}
void ZPhysicalMemoryManager::map_view(uintptr_t addr, const ZPhysicalMemory& pmem) const {
  size_t size = 0;
  // Map segments
  for (uint32_t i = 0; i < pmem.nsegments(); i++) {
    //zgc內部把物理內存分紅一個個segment
    const ZPhysicalMemorySegment& segment = pmem.segment(i);
   //每一個segment從開始到結束進行映射
    _backing.map(addr + size, segment.size(), segment.start());
    size += segment.size();
  }
  .....
}
void ZPhysicalMemoryBacking::map(uintptr_t addr, size_t size, uintptr_t offset) const {
  //最終調用mmap函數映射內存
  const void* const res = mmap((void*)addr, size, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_SHARED, _fd, offset);
  if (res == MAP_FAILED) {
    ZErrno err;
    fatal("Failed to map memory (%s)", err.to_string());
  }
}

        只看源碼有些枯燥,咱們畫個圖來整理下邏輯關係:

        

       由此咱們能夠畫出對應的關係:

       
   

        到這裏,咱們能夠總結下,染色指針不光是把標記信息存儲在指針上,還對物理內存進行了多重映射,同一時間只存在一個視圖,當咱們訪問對象時,只須要判斷其指針的標誌位是不是當前視圖下的好指針,就能夠判斷其標記狀況,性能提高是巨大的,這裏說到底仍是空間換時間的思路。

        特性二:讀屏障

        讀屏障相比較來講比較好理解,傳統gc使用的都是寫屏障,去解決標記對象時漏標的問題,這部分會涉及三色標記和漏標的知識點,網上文章比較多,筆者就不過多闡述。ZGC則是使用的讀屏障,在訪問對象以前咱們只要判斷對象的引用標誌位,對象是不是處於移動後,不須要整個gc過程結束,這樣能夠大大減小停頓時間。每次訪問對象,由於由染色指針的技術,也能夠在很是段的時間內判斷對象的標誌,因此使用讀屏障並不會影響性能。

        咱們來看下其主要方法:

        能夠看到其主要邏輯是訪問對象時,只須要判斷其指針在當前視圖下是否是好指針,若是是則證實其能夠正常訪問,直接返回,若是不是則判斷是否是正在移動中,若是是則等待其移動,若是不是,對其進行指針修復,修復到正確的地址上(修復的方法咱們下一章再講,這裏先簡略)。

//讀屏障
address ZBarrierSetRuntime::load_barrier_on_oop_field_preloaded_addr(DecoratorSet decorators) {
  //判斷是什麼引用
  if (decorators & ON_PHANTOM_OOP_REF) {
    return load_barrier_on_phantom_oop_field_preloaded_addr();
  } else if (decorators & ON_WEAK_OOP_REF) {
    return load_barrier_on_weak_oop_field_preloaded_addr();
  } else {
   //先看這裏
    return load_barrier_on_oop_field_preloaded_addr();
  }
}
//這裏傳入兩個閉包函數
inline oop ZBarrier::load_barrier_on_oop_field_preloaded(volatile oop* p, oop o) {
  return barrier<is_good_or_null_fast_path, load_barrier_on_oop_slow_path>(p, o);
}

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

  //這裏調第一個閉包,第一個會判斷是不是當前視圖下的好指針
  if (fast_path(addr)) {
    return ZOop::from_address(addr);
  }

  //這裏調第二個閉包
  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);
}

//第二個閉包,這裏會判斷對象是否在移動中,若是是則進行移動,若是不是則標記並修改指針到新對象
uintptr_t ZBarrier::relocate_or_mark(uintptr_t addr) {
  return during_relocate() ? relocate(addr) : mark<Follow, Strong, Publish>(addr);
}

        到這裏,咱們如今知道了zgc的染色指針和讀屏障具體是怎麼回事,可是筆者不由又對視圖產生了好奇,gc的過程當中是如何切換視圖?gc過程對象是如何搬運的?

        zgc過程解讀

        先上代碼(jdk15中gc是9步,其餘版本可能有10步):

        這裏是gc方法的源碼,咱們能夠看到有清晰的9步

//zgc過程 9步
void ZDriver::gc(GCCause::Cause cause) {
  ZDriverGCScope scope(cause);

  // Phase 1: Pause Mark Start 初始標記
 //調用pause<VM_ZMarkStart>();全局停頓
//最終調用void ZHeap::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 Process Non-Strong References
  concurrent_process_non_strong_references();

  // Phase 5: Concurrent Reset Relocation Set
  concurrent_reset_relocation_set();

  // Phase 6: Pause Verify 驗證GC狀態
  pause_verify();

  // Phase 7: Concurrent Select Relocation Set
  concurrent_select_relocation_set();

  // Phase 8: Pause Relocate Start 
  // 全局停頓
  pause_relocate_start();

  // Phase 9: Concurrent Relocate 併發回收
  concurrent_relocate();
}

        具體的代碼比較繁多,直接看的話可能會有些概念不太清楚,筆者先畫圖來輔助你們理解,並在其中解釋一些概念,以後筆者會貼上對應的代碼進行講解學習:

1.初始標記

 初始標記會從GcRoot出發快速修改相關引用的指針,這一步表明GcRoot直接引用的對象已經被標記,其指針已經被染色爲當前視圖下的指針。

 jvm初始化時建立的指針都是remapped視圖下的,而marked標記(用來表示當前marked輪次的,後面源碼中會看到)則是marked0,在第一次進行marked視圖切換時,因爲marked標記是marked0,因此會從marked0切換到marked1,因此這裏視圖切換是從remapped切換成marked1。1 2 4對象已是marked1,其餘對對象仍是remapped。這裏會有第一次全局停頓。

 

2.併發標記

這裏會繼續遍歷整個堆中的存活對象,並將其指針進行染色。5 8 的指針也會被染色爲當前視圖下指針,3 6 7 則不變,進行標記的時候還會在每一個頁中記錄下存活對象的字節數大小,方便後面選擇遷移頁。還有一點,這個階段還會修復壞指針,因爲演示的是第一次GC狀況,因此圖中不會展現出來,後面第二次GC就能夠看到指針修復的狀況。

 

3.從新標記

這個階段會作一些收尾工做,會把併發標記階段未處理完的任務繼續處理完,若沒有處理完則繼續進行併發標記,若是處理完了則繼續進行下一步,這個階段會控制在1ms內,超過1ms會繼續併發標記。這個階段是第二次全局停頓

 

4.併發遷移準備

這個階段在源碼中是這4步:

// Phase 4: Concurrent Process Non-Strong References
  concurrent_process_non_strong_references();

  // Phase 5: Concurrent Reset Relocation Set
  // 重置遷移集合
  concurrent_reset_relocation_set();

  // Phase 6: Pause Verify 驗證GC狀態
  pause_verify();

  // Phase 7: Concurrent Select Relocation Set
  // 選擇須要遷移的集合
  concurrent_select_relocation_set();

這一步會先清空Forwarding Table(用來記錄遷移對象後的映射關係),以後再清空RelocationSet。

而後再根據必定規則去選擇須要遷移的頁,若是頁中有存活的對象(能夠理解爲在進行標記的時候還會對頁進行標記標記其爲存活頁,後面源碼分析中會提到),則註冊成存活頁,若是沒有存活對象則直接回收頁。最後再從存活頁中按必定策略選擇須要遷移的頁並按必定順序填充進遷移集合。(ZGC中的頁分爲,大頁,中頁,小頁,這裏規則是中頁在前,小頁在後,每一個頁組將按存活對象字節數升序進行排序)填充的操做實際上是將頁的信息封裝成一個個Forwarding存到RlocationSet中並排序。

填充後會將這些Forwarding加入到Forwarding Table中,此時這裏面還只有須要遷移對象的信息。

 

5.初始遷移

這個階段會先切換視圖,將視圖切換到remapped視圖,以後會掃描與根節點相關的對象,判斷其指針是好是壞,若是是好指針則直接返回。若是是壞指針,則會先判斷是否在Forwarding Tables中有其信息,若是沒有就表明不須要遷移,則直接標記成當前視圖下好指針並返回,若是有則表明須要進行遷移。上圖中1 ,2 均不在Forwarding Tables中,則直接指針被染色,而4則進行遷移。

先申請塊內存,而後將對象copy過去,以後把映射關係記錄在Forwarding Table中,最後修改gcRoot指針到新對象上。這個過程只掃描少數對象,過程很是快,是全局停頓的。

 

6.併發遷移

在這個階段會先遍歷RelocationSet中全部的forwarding,從中獲取須要回收的頁信息,從頁信息中遍歷存活的對象,並對其進行遷移

以後會根據存活對象大小申請新的內存並將對象copy過去,而後在forwarding中記錄映射關係,forwarding同時會反應在Forwarding Tables中,圖中 5 8 都進行了copy但其顏色還未改變,這時候會體現ZGC的優點,若是應用訪問這兩個對象,則會觸發讀屏障,快速的判斷其指針顏色發現是壞指針而後修復指針。(只有第一次訪問的時候會修復,後面都會變成好指針)

最後則會把以前relocationSet中記錄的頁進行回收,這時候紅色箭頭的都是失效的指針都是壞指針,若是用戶訪問這些指針會觸發讀屏障進行指針修復。

7.第一次GC結束,第二次GC前

以上第一次GC過程結束,以後一些壞指針會在用戶訪問以後進行修復,可是有些指針可能沒有被訪問到,則會在第二次GC的時候進行修復。上圖對象4到5的指針會由於讀屏障修復,因此指針被染色成remapped。

8.初次標記(第二次GC)

第二次GC的初次標記階段,因爲以前的marked標記是1,如今會切換到0,因此視圖是從remapped切換到marked0,因此1 2 4 的指針都被染色成marked0

9.併發標記(第二次GC)

第二此併發標記會將沒有被讀屏障修復的指針進行修復並染色,如今1 2 4 5 8 都被染成marked0視圖的指針

10.併發遷移準備(第二次GC)

在第二次GC的併發遷移準備階段,會將以前保存的relocationSet和Forwarding Table都清空。以後的階段就和第一次GC同樣,在這裏就不進行畫圖描述了。

 

        GC階段源碼

        上圖是筆者根據源碼學習後畫出的,可是隻看圖和過程講解仍是不夠深入,源碼中還有不少細節是圖不能表現的,圖只能幫助咱們理解輪廓,下面咱們一塊兒看下源碼是如何進行GC的,這裏再把GC9步的源碼方法先貼出來

//zgc過程 9步
void ZDriver::gc(GCCause::Cause cause) {
  ZDriverGCScope scope(cause);

  // Phase 1: Pause Mark Start 初始標記
 //調用pause<VM_ZMarkStart>();全局停頓
//最終調用void ZHeap::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 Process Non-Strong References
  concurrent_process_non_strong_references();

  // Phase 5: Concurrent Reset Relocation Set
  concurrent_reset_relocation_set();

  // Phase 6: Pause Verify 驗證GC狀態
  pause_verify();

  // Phase 7: Concurrent Select Relocation Set
  concurrent_select_relocation_set();

  // Phase 8: Pause Relocate Start 
  // 全局停頓
  pause_relocate_start();

  // Phase 9: Concurrent Relocate 併發回收
  concurrent_relocate();
}

       

        1.初次標記源碼:      

初次標記方法通過一連串調用,最後會進入這個方法

//初次標記最後會調這個方法
void ZHeap::mark_start() {
  assert(SafepointSynchronize::is_at_safepoint(), "Should be at safepoint");
  //更新統計數據(提供給GC日誌的統計數據,後文都不在作解釋)
  ZStatSample(ZSamplerHeapUsedBeforeMark, used());
  // 切換內存映射視圖
  // remapp視圖切換到marked0,marked1視圖
  // 第一次的話是切到M1視圖
  flip_to_marked();
  //重置對象頁數據
  _object_allocator.retire_pages();
  //重置頁數據
  _page_allocator.reset_statistics();
 //重置引用數據
  _reference_processor.reset_statistics();
  //修改標記位
  ZGlobalPhase = ZPhaseMark;
  //初始標記,標記根節點
  _mark.start();
  //更新統計數據
  ZStatHeap::set_at_mark_start(soft_max_capacity(), capacity(), used());
}

先來看看filp_toMarked()切換視圖的方法:

//切換marked視圖
void ZAddress::flip_to_marked() {
  //這個就是上文提到marked標記
  ZAddressMetadataMarked ^= (ZAddressMetadataMarked0 | ZAddressMetadataMarked1);
  //將marked標記設置成好指針標記
  set_good_mask(ZAddressMetadataMarked);
}

能夠看出marked標記會再marked0和marked1之間互相轉換,當marked標記爲marked0時,調用這個方法後會變成marked1,反之則會變成marked0。

跳過幾個重置頁和數據的方法,咱們直接看標記根節點的方法_mark.start(),方法主要是遍歷根節點,中間調用了幾個迭代器類,這裏就不列出,咱們直接看遍歷對象的方法:

//遍歷的方法是ZMarkRootsIteratorClosure這個類中的
//傳入的是gc中的根節點直接引用的對象,包括棧裏的引用和一些VM靜態數據指向堆中的引用,這裏就不詳細列舉
virtual void do_oop(oop* p) {
   ZBarrier::mark_barrier_on_root_oop_field(p);
}

inline void ZBarrier::mark_barrier_on_root_oop_field(oop* p) {
  const oop o = *p;
  //這個方法會傳入兩個閉包方法
  root_barrier<is_good_or_null_fast_path, mark_barrier_on_root_oop_slow_path>(p, o);
}

template <ZBarrierFastPath fast_path, ZBarrierSlowPath slow_path>
inline void ZBarrier::root_barrier(oop* p, oop o) {
  //獲取地址
  const uintptr_t addr = ZOop::to_address(o);
  //第一個閉包方法,這個閉包方法就是判斷指針是不是好指針,這裏就再也不論述
  if (fast_path(addr)) {
    return;
  }
  //第二個閉包方法,返回當前視圖下的好指針,其實是指針染色
  const uintptr_t good_addr = slow_path(addr);
  //將染色後的指針轉化爲地址並修改以前的指針
  *p = ZOop::from_address(good_addr);
}

//第二個閉包方法
uintptr_t ZBarrier::mark_barrier_on_root_oop_slow_path(uintptr_t addr) {
  //標記並返回染色指針
  return mark<Follow, Strong, Publish>(addr);
}
//這個方法是指針染色的核心方法
template <bool follow, bool finalizable, bool publish>
uintptr_t ZBarrier::mark(uintptr_t addr) {
  uintptr_t good_addr;

  if (ZAddress::is_marked(addr)) {
    // 判斷是不是當前marked視圖下的指針,是則直接返回好指針(染色)
    good_addr = ZAddress::good(addr);
  } else if (ZAddress::is_remapped(addr)) {
    // 這個階段主要會進入這個分支
    // 判斷是不是remapped視圖下的指針,直接返回當前視圖的好指針(染色)
    good_addr = ZAddress::good(addr);
  } else {
    // 這個階段不會進入這個分支,這裏說明是壞指針,會進行指針修復和染色
    good_addr = remap(addr);
  }

  // 這裏的標記方法會將標記任務先放到一個stack中,等到併發標記時再處理
  if (should_mark_through<finalizable>(addr)) {
    ZHeap::heap()->mark_object<follow, finalizable, publish>(good_addr);
  } 

  ...

  return good_addr;
}

不難看出,這個階段雖然是會全局停頓,可是實際上只是對根節點直接引用的對象指針進行染色處理,大部分指針是由remapped視圖被染色成maked視圖(0或者1)

        2.併發標記源碼:

併發標記最終會進入這個方法:

void ZMark::mark(bool initial) {
  //這個標識符是判斷是否是第一次進入併發標記
  //上文說到初始標記結束會判斷併發標記是否成功,若是沒成功獲取超過1ms則會再次進入併發標記階段,再次進入的併發標
  //標記階段此標識符就是false
  if (initial) {
    //併發標記任務
    ZMarkConcurrentRootsTask task(this);
    _workers->run_concurrent(&task);
  }
  //標記任務
  //這裏會處理上文提到的stack中的任務進行標記,其實是在對象所處的頁中的存活map中標記出來
  ZMarkTask task(this);
  _workers->run_concurrent(&task);
}

先來看看併發標記任務,其中也會通過一些迭代器工具,和初次標記同樣,咱們主要看看遍歷對象的方法:

//最終調用ZMarkConcurrentRootsIteratorClosure迭代器的方法
virtual void do_oop(oop* p) {
   ZBarrier::mark_barrier_on_oop_field(p, false /* finalizable */);
}
//標記方法
inline void ZBarrier::mark_barrier_on_oop_field(volatile oop* p, bool finalizable) {
  //原子類加載對象
  const oop o = Atomic::load(p);
  //這裏false
  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_barrier_on_oop_slow_path(addr);
    } else {
      //壞指針則須要修復並染色以後再標記
      //再看到這個方法已經很熟悉了,咱們直接來看第二個閉包方法
      barrier<is_good_or_null_fast_path, mark_barrier_on_oop_slow_path>(p, o);
    }
  }
}

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

  if (ZAddress::is_marked(addr)) {
    good_addr = ZAddress::good(addr);
  } else if (ZAddress::is_remapped(addr)) {
    //若是是remapped視圖則會在此進行染色變成marked視圖
    good_addr = ZAddress::good(addr);
  } else {
    //這個階段可能會有指針走這個分支進行指針修復,主要是上個階段GC留下的沒有被讀屏障修復過的壞指針
    good_addr = remap(addr);
  }

  // 和初次標記同樣,先把對象信息封裝到一個stack裏
  if (should_mark_through<finalizable>(addr)) {
    ZHeap::heap()->mark_object<follow, finalizable, publish>(good_addr);
  }

  ...

  return good_addr;

咱們來看看一直提到的修復指針方法:

//最終會調用這個方法去修復指針
inline uintptr_t ZHeap::remap_object(uintptr_t addr) {
  //從forwarding_table中獲取映射關係
  ZForwarding* const forwarding = _forwarding_table.get(addr);
  if (forwarding == NULL) {
    // Not forwarding
    return ZAddress::good(addr);
  }

  return _relocate.forward_object(forwarding, addr);
}

uintptr_t ZRelocate::forward_object(ZForwarding* forwarding, uintptr_t from_addr) const {
  const uintptr_t from_offset = ZAddress::offset(from_addr);
  const uintptr_t from_index = (from_offset - forwarding->start()) >> forwarding->object_alignment_shift();
  //從forwarding中找到對應對象信息
  const ZForwardingEntry entry = forwarding->find(from_index);
  //返回新的指針並染色
  return ZAddress::good(entry.to_offset());
}

在以前畫圖階段咱們已經提到過forwarding_table和forwarding,其最終記錄了遷移指針的映射關係,entry.to_offset()返回的就是到遷移後對象的指針,這裏直接進行染色並返回,以後會替換調原壞指針,因此是修復並染色的過程。

以上根節點和全部存活對象的指針染色過程就完成了,咱們來看看剛纔提到過的標記過程,剛纔說到標記方法實際上是將對象信息封裝到一個stack裏面,而後再併發標記階段的標記任務,下面咱們看下標記任務:

//標記任務最後調用這個方法
void ZMark::work(uint64_t timeout_in_millis) {
  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_millis == 0) {
    //直接標記
    work_without_timeout(&cache, stripe, stacks);
  } else {
    //有超時時間的標記
    work_with_timeout(&cache, stripe, stacks, timeout_in_millis);
  }
  // 清空Stack
  stacks->free(&_allocator);
}

//直接標記和超時時間標記顧名思義區別再有超時時間,咱們看下直接標記的方法
//最後會跳到這
template <typename T>
bool ZMark::drain(ZMarkStripe* stripe, ZMarkThreadLocalStacks* stacks, ZMarkCache* cache, T* timeout) {
  ZMarkStackEntry entry;
  //從stack中出棧
  while (stacks->pop(&_allocator, &_stripes, stripe, entry)) {
    //標記方法
    mark_and_follow(cache, entry);

    處理超時時間
    if (timeout->has_expired()) {
      return false;
    }
  }
  return true;
}
//再通過幾個方法調用會到這裏標記對象的方法
inline bool ZPage::mark_object(uintptr_t addr, bool finalizable, bool& inc_live) {
  const size_t index = ((ZAddress::offset(addr) - start()) >> object_alignment_shift()) * 2;
  //咱們看到實際上是再zPage中的liveMap進行標記
  return _livemap.set(index, finalizable, inc_live);
}

如今咱們知道了標記會再頁中的存活對象map中進行記錄,後面在選擇遷移頁時會使用到,以後咱們還會看處處理的代碼。至此併發標記階段結束

        3.初始標記結束源碼

//這個階段會試着結束標記階段,若是沒有成功則繼續進行併發標記
bool ZHeap::mark_end() {
//結束標記工做
//這裏會flush,本地線程棧
  if (!_mark.end()) {
    return false;
  }
//更改標識符,進入標記完成階段
  ZGlobalPhase = ZPhaseMarkCompleted;

  ZVerify::after_mark();
//更新統計數據
  ZStatSample(ZSamplerHeapUsedAfterMark, used());
  ZStatHeap::set_at_mark_end(capacity(), allocated(), used());
  ZResurrection::block();
 //處理弱引用
  _weak_roots_processor.process_weak_roots();
  // 準備卸載過時的元數據和nmethods
  _unload.prepare();

  return true;
}

這個階段比較簡單,是全局停頓的,在_mark.end()方法中會處理前面提到的stack中的任務,若是超時則會返回false從新回到併發標記階段。

        4.準備遷移階段源碼

前面提到這個階段的方法涉及4個:

// Phase 4:處理一些非強引用,非本文重點,這裏就不論述了
  concurrent_process_non_strong_references();

  // Phase 5: 重置遷移集合
  concurrent_reset_relocation_set();

  // Phase 6: 驗證GC狀態,非本文重點,這裏就不論述了
  pause_verify();

  // Phase 7: 選擇遷移集合
  concurrent_select_relocation_set();

咱們先看看重置遷移集合:

//重置遷移集合
void ZHeap::reset_relocation_set() {
  //初始化一個空的forwarding table用來保存遷移先後的關係
  ZRelocationSetIterator iter(&_relocation_set);
  for (ZForwarding* forwarding; iter.next(&forwarding);) {
    _forwarding_table.remove(forwarding);
  }
  //重置回收集合
  _relocation_set.reset();
}

邏輯比較簡單,就是先刪除_forwarding_table中的forwarding,以後再清空relocationSet

再來看看選擇遷移集合方法:

//選擇遷移集合
void ZHeap::select_relocation_set() {
  //不容許頁被刪除,底層爲加個鎖(非本文重點)
  _page_allocator.enable_deferred_delete();

  // 註冊一個遷移頁選擇器
  ZRelocationSetSelector selector;
  ZPageTableIterator pt_iter(&_page_table);
  //循環全部頁
  for (ZPage* page; pt_iter.next(&page);) {
    //若是已經被標記要遷移
    if (!page->is_relocatable()) {
     //不用回收
      continue;
    }
    //判斷page是否被標記過,這裏就是看頁中liveMap是否有數據
    if (page->is_marked()) {
     //註冊爲存活頁
      selector.register_live_page(page);
    } else {
      //註冊爲垃圾頁
      selector.register_garbage_page(page);
      //馬上回收沒有存活對象的垃圾頁
      free_page(page, true /* reclaimed */);
    }
  }
  //釋放刪除頁的鎖(非本文重點)
  _page_allocator.disable_deferred_delete();
  // 選擇頁去回收,使用策略計算須要回收的頁放入回收集合
  selector.select(&_relocation_set);
  // 向forwarding table加入fowarding記錄
  ZRelocationSetIterator rs_iter(&_relocation_set);
  for (ZForwarding* forwarding; rs_iter.next(&forwarding);) {
    _forwarding_table.insert(forwarding);
  }
  // 更新統計數據
  ZStatRelocation::set_at_select_relocation_set(selector.stats());
  ZStatHeap::set_at_select_relocation_set(selector.stats(), reclaimed());
}

邏輯也比較簡單,遍歷全部頁,判斷頁是否須要回收,依據就是前文提到的liveMap中是否有數據,若是liveMap中沒有數據則能夠直接回收,有則註冊到選擇器中。而後會對回收頁進行選擇排序,咱們看下源碼

void ZRelocationSetSelector::select(ZRelocationSet* relocation_set) {

  EventZRelocationSet event;

  // 選則器中會將大,中,小頁分爲三個組這裏先再組內按存活對象大小字節數排序
  _large.select();
  _medium.select();
  _small.select();

  // 而後進行填充,這裏先填充中頁,後小頁(大頁並無回收,這裏先不論述)
  relocation_set->populate(_medium.selected(), _medium.nselected(),
                           _small.selected(), _small.nselected());

  //zgc的異步支持,事件處理器,這裏非本文重點就不論述了
  event.commit(total(), empty(), compacting_from(), compacting_to());
}
//填充方法
void ZRelocationSet::populate(ZPage* const* group0, size_t ngroup0,
                              ZPage* const* group1, size_t ngroup1) {
  _nforwardings = ngroup0 + ngroup1;
  _forwardings = REALLOC_C_HEAP_ARRAY(ZForwarding*, _forwardings, _nforwardings, mtGC);

  size_t j = 0;

  // 填充方法就是將頁信息封裝成forwarding加入relocationSet的forwarding數組中
  for (size_t i = 0; i < ngroup0; i++) {
    _forwardings[j++] = ZForwarding::create(group0[i]);
  }
  for (size_t i = 0; i < ngroup1; i++) {
    _forwardings[j++] = ZForwarding::create(group1[i]);
  }
}

看到這裏應該能夠應證咱們以前畫的圖,relocationSet中的forwarding信息又被遍歷而後加入到forwarding Table中,至此遷移準備階段結束。

        5.初始遷移源碼

//初始遷移的方法
void ZHeap::relocate_start() {
  assert(SafepointSynchronize::is_at_safepoint(), "Should be at safepoint");

  _unload.finish();

  // 切換視圖到remapped
  flip_to_remapped();

  // 修改gc標識符
  ZGlobalPhase = ZPhaseRelocate;

  // 更新gc統計數據
  ZStatSample(ZSamplerHeapUsedBeforeRelocation, used());
  ZStatHeap::set_at_relocate_start(capacity(), allocated(), used());

  // 遷移gcRoot直接引用的對象
  _relocate.start();
}

遷移gcRoot引用的對象和標記時同樣會調用許多層迭代器,咱們直接來看遷移的方法:

//ZRelocateRootsIteratorClosure迭代器的方法
virtual void do_oop(oop* p) {
  ZBarrier::relocate_barrier_on_root_oop_field(p);
}
//又是似曾相識的方法,但其實不同這此有遷移的邏輯
//這裏只有第二個閉包方法不同,咱們直接來看看
inline void ZBarrier::relocate_barrier_on_root_oop_field(oop* p) {
  const oop o = *p;
  root_barrier<is_good_or_null_fast_path, relocate_barrier_on_root_oop_slow_path>(p, o);
}

//第二個閉包方法,不是好指針的狀況下會到這裏,若是是好指針則直接返回染色的指針
uintptr_t ZBarrier::relocate_barrier_on_root_oop_slow_path(uintptr_t addr) {
  // 簡單幹脆直接遷移
  return relocate(addr);
}
//最後會調這個方法
inline uintptr_t ZHeap::relocate_object(uintptr_t addr) {
  //先判斷時候在forwarding_table中
  ZForwarding* const forwarding = _forwarding_table.get(addr);
  if (forwarding == NULL) {
    // Not forwarding
    return ZAddress::good(addr);
  }
  const bool retained = forwarding->retain_page();
  //進行遷移
  const uintptr_t new_addr = _relocate.relocate_object(forwarding, addr);
  if (retained) {
    forwarding->release_page();
  }

  return new_addr;
}

//真正的遷移方法
uintptr_t ZRelocate::relocate_object_inner(ZForwarding* forwarding, uintptr_t from_index, uintptr_t from_offset) const {
  ZForwardingCursor cursor;
  
  ...

  // 申請新的內存
  const uintptr_t from_good = ZAddress::good(from_offset);
  const size_t size = ZUtils::object_size(from_good);
  // 這裏會從空閒的頁中申請新的內存,具體就再也不論述
  const uintptr_t to_good = ZHeap::heap()->alloc_object_for_relocation(size);
  if (to_good == 0) {
    return forwarding->insert(from_index, from_offset, &cursor);
  }

  // 拷貝對象
  ZUtils::object_copy(from_good, to_good, size);

  // 保存forwarding記錄映射關係
  // 對象地址被標記位remap的視圖
  const uintptr_t to_offset = ZAddress::offset(to_good);
  const uintptr_t to_offset_final = forwarding->insert(from_index, to_offset, &cursor);
  if (to_offset_final == to_offset) {
    return to_offset;
  }
  
  ...

  return to_offset_final;
}

這個階段會遍歷gcRoot直接引用的對象指針,判斷指針是好是壞,若是是好指針則染色並返回,這部分的方法和以前同樣,筆者就沒貼出來。若是是壞指針則會將其進行遷移,在空閒的頁中申請一塊內存將對象copy過去,而後在forwarding中插入映射關係。

        5.併發遷移源碼

併發遷移和併發標記同樣,也會通過幾個迭代器的調用,咱們直接貼出核心代碼:

//最後調用這個方法
bool ZRelocate::work(ZRelocationSetParallelIterator* iter) {
  bool success = true;
  // 遍歷relocationSet中的forwarding數組
  for (ZForwarding* forwarding; iter->next(&forwarding);) {
    // 從forwarding中獲取頁信息並把迭代器傳進入遍歷頁中liveMap中的對象
    ZRelocateObjectClosure cl(this, forwarding);
    forwarding->page()->object_iterate(&cl);

    if (ZVerifyForwarding) {
      forwarding->verify();
    }
    //判斷頁是不是固定的
    if (forwarding->is_pinned()) {
      success = false;
    } else {
      // 不是的話則進行回收
      forwarding->release_page();
    }
  }
  return success;
}
//object_iterate方法實際上是遍歷頁中的liveMap中的對象
inline void ZPage::object_iterate(ObjectClosure* cl) {
  _livemap.iterate(cl, ZAddress::good(start()), object_alignment_shift());
}

到這裏咱們知道這個階段會逐個遍歷須要回收的頁中的livemap中的對象,咱們看看ZRelocateObjectClosure 遷移迭代器的迭代方法:

virtual void do_object(oop o) {
  _relocate->relocate_object(_forwarding, ZOop::to_address(o));
}
//熟悉的配方,熟悉的味道
uintptr_t ZRelocate::relocate_object(ZForwarding* forwarding, uintptr_t from_addr) const {
  const uintptr_t from_offset = ZAddress::offset(from_addr);
  const uintptr_t from_index = (from_offset - forwarding->start()) >> forwarding->object_alignment_shift();
  const uintptr_t to_offset = relocate_object_inner(forwarding, from_index, from_offset);

  if (from_offset == to_offset) {
    forwarding->set_pinned();
  }

  return ZAddress::good(to_offset);
}

又回到了gcRoot遷移的方法,這裏就再也不次論述了,至此整個GC過程的源碼就結束了。相信你們看到此處應該知道爲什麼筆者要先畫圖在理解,若是直接看源碼確實會陷入一個一個問題陷阱,因此筆者在畫圖時已經把須要關注的點和一些問題標註出來,等到咱們學習源碼的時候能夠結合以前的圖進行學習,這樣能夠更加清晰的理解和學習。

        總結:

        通過這次對ZGC源碼的學習,咱們對染色指針染的是什麼,讀屏障爲何效率這麼高,ZGC停頓在哪裏,爲何停頓時間這麼短,應該有了更深入的理解,對生產環境使用ZGC也有了更多的信心,只有清楚其原理和構成,咱們才能夠放心的使用。

        筆者也不禁得發出感概,與其看千百遍文章不如本身去讀讀源碼,源碼中蘊含這許多精妙的設計和細節。固然筆者限於水平,可能有些地方理解還不夠徹底,歡迎你們指出。

相關文章
相關標籤/搜索