从源码出发看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也有了更多的信心,只有清楚其原理和构成,咱们才能够放心的使用。

        笔者也不禁得发出感概,与其看千百遍文章不如本身去读读源码,源码中蕴含这许多精妙的设计和细节。固然笔者限于水平,可能有些地方理解还不够彻底,欢迎你们指出。

相关文章
相关标签/搜索