性能文章>从源码出发看zgc的技术内幕>

从源码出发看zgc的技术内幕原创

1年前
399517

笔者经过上次对zgc在不同环境下进行的测试后,发现zgc所带来的提升非常之大。一时间对zgc在生产中使用充满信心,但是在全面使用之前,难免对其几大新特性有一些好奇,比如:染色指针,读屏障,动态region,支持NUMA等等。其中有一些是比较好理解的,但是有一些例如染色指针,读屏障刚接触的时候会不明其意。在网上搜索一番后发现很多文档都只是简单一笔盖过,或者只介绍个概念,甚至还有错误或者模糊的介绍,具体的实现和意义最让笔者好奇,但又找不到答案。所以笔者在经过一段时间的ZGC源码学习后,在此做一番总结。

特性一:染色指针

我们都知道jvm的垃圾回收器回收过程中都涉及到对对象进行标记,只有标记过的对象才是存活的对象,未被标记的对象将在GC中被回收掉。zgc的对象标记实现用的则是染色指针技术。(传统的GC都是将标记记录在对象头中,G1则是将标记记录在与对象独立的数据结构上-----Rset)

话不多说先看一张图:
111.png

从图中我们可以得知zgc在64位操作系统的虚拟地址中取了4个bit进行标记,分别是

43—Marked0,44—Marked1,45—Remapped,46—Finalizeble(这里可以先简单了解,后面会具体讲不同标志位的意义)

前42位(jdk15中jvm会根据堆设置大小控制长度42-44位)则是具体的对象地址,后18位无意义。

在GC过程中会对对象的指针进行标记,这样就可以在GC标记追踪对象的时候只与指针打交道,而不用管对象本身。

以上就是在网上或者其他文档中可以搜索到的关于染色指针的解读,笔者了解下来仍旧对其很模糊,所以下面我们看下源码更深刻的理解染色指针技术的意义。

1.1 染色指针源码解读(基于openjdk15):

我们先看下创建对象返回指针的方法:

这里的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方便管理

222.png

由此我们可以找到指针映射的入口:

//入口
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());
  }
}

只看源码有些枯燥,我们画个图来整理下逻辑关系:
333.png
由此我们可以画出对应的关系:
444.png
到这里,我们可以总结下,染色指针不光是把标记信息存储在指针上,还对物理内存进行了多重映射,同一时间只存在一个视图,当我们访问对象时,只需要判断其指针的标志位是否是当前视图下的好指针,就可以判断其标记情况,性能提升是巨大的,这里说到底还是空间换时间的思路。

特性二:读屏障

读屏障相比较来说比较好理解,传统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();
}

具体的代码比较繁多,直接看的话可能会有些概念不太清楚,笔者先画图来辅助大家理解,并在其中解释一些概念,之后笔者会贴上对应的代码进行讲解学习:
555.png

1.初始标记

初始标记会从GcRoot出发快速修改相关引用的指针,这一步代表GcRoot直接引用的对象已经被标记,其指针已经被染色为当前视图下的指针。

jvm初始化时创建的指针都是remapped视图下的,而marked标记(用来表示当前marked轮次的,后面源码中会看到)则是marked0,在第一次进行marked视图切换时,由于marked标记是marked0,所以会从marked0切换到marked1,所以这里视图切换是从remapped切换成marked1。1 2 4对象已经是marked1,其他对对象还是remapped。这里会有第一次全局停顿。
666.png

2.并发标记

这里会继续遍历整个堆中的存活对象,并将其指针进行染色。5 8 的指针也会被染色为当前视图下指针,3 6 7 则不变,进行标记的时候还会在每个页中记录下存活对象的字节数大小,方便后面选择迁移页。还有一点,这个阶段还会修复坏指针,由于演示的是第一次GC情况,所以图中不会展示出来,后面第二次GC就可以看到指针修复的情况。
777.png

3.重新标记

这个阶段会做一些收尾工作,会把并发标记阶段未处理完的任务继续处理完,若没有处理完则继续进行并发标记,如果处理完了则继续进行下一步,这个阶段会控制在1ms内,超过1ms会继续并发标记。这个阶段是第二次全局停顿
888.png

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。
999.png

然后再根据一定规则去选择需要迁移的页,如果页中有存活的对象(可以理解为在进行标记的时候还会对页进行标记标记其为存活页,后面源码分析中会提到),则注册成存活页,如果没有存活对象则直接回收页。最后再从存活页中按一定策略选择需要迁移的页并按一定顺序填充进迁移集合。(ZGC中的页分为,大页,中页,小页,这里规则是中页在前,小页在后,每个页组将按存活对象字节数升序进行排序)填充的操作其实是将页的信息封装成一个个Forwarding存到RlocationSet中并排序。
10.png

填充后会将这些Forwarding加入到Forwarding Table中,此时这里面还只有需要迁移对象的信息。
11.png

5.初始迁移

这个阶段会先切换视图,将视图切换到remapped视图,之后会扫描与根节点相关的对象,判断其指针是好是坏,如果是好指针则直接返回。如果是坏指针,则会先判断是否在Forwarding Tables中有其信息,如果没有就代表不需要迁移,则直接标记成当前视图下好指针并返回,如果有则代表需要进行迁移。上图中1 ,2 均不在Forwarding Tables中,则直接指针被染色,而4则进行迁移。
12.png

先申请块内存,然后将对象copy过去,之后把映射关系记录在Forwarding Table中,最后修改gcRoot指针到新对象上。这个过程只扫描少数对象,过程非常快,是全局停顿的。
13.png

6.并发迁移

在这个阶段会先遍历RelocationSet中所有的forwarding,从中获取需要回收的页信息,从页信息中遍历存活的对象,并对其进行迁移
14.png

之后会根据存活对象大小申请新的内存并将对象copy过去,然后在forwarding中记录映射关系,forwarding同时会反应在Forwarding Tables中,图中 5 8 都进行了copy但其颜色还未改变,这时候会体现ZGC的优势,如果应用访问这两个对象,则会触发读屏障,快速的判断其指针颜色发现是坏指针然后修复指针。(只有第一次访问的时候会修复,后面都会变成好指针)
15.png

最后则会把之前relocationSet中记录的页进行回收,这时候红色箭头的都是失效的指针都是坏指针,如果用户访问这些指针会触发读屏障进行指针修复。

7.第一次GC结束,第二次GC前
16.png

以上第一次GC过程结束,之后一些坏指针会在用户访问之后进行修复,但是有些指针可能没有被访问到,则会在第二次GC的时候进行修复。上图对象4到5的指针会因为读屏障修复,所以指针被染色成remapped。

8.初次标记(第二次GC)
17.png

第二次GC的初次标记阶段,由于之前的marked标记是1,现在会切换到0,所以视图是从remapped切换到marked0,所以1 2 4 的指针都被染色成marked0

9.并发标记(第二次GC)
18.png

第二此并发标记会将没有被读屏障修复的指针进行修复并染色,现在1 2 4 5 8 都被染成marked0视图的指针

10.并发迁移准备(第二次GC)
19.png

在第二次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也有了更多的信心,只有清楚其原理和构成,我们才可以放心的使用。

笔者也不由得发出感概,与其看千百遍文章不如自己去读读源码,源码中蕴含这许多精妙的设计和细节。当然笔者限于水平,可能有些地方理解还不够完全,欢迎大家指出。

点赞收藏
分类:标签:
自成溪
请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

记一次线上RPC超时故障排查及后续GC调优思路

记一次线上RPC超时故障排查及后续GC调优思路

解读JVM级别本地缓存Caffeine青出于蓝的要诀 —— 缘何会更强、如何去上手

解读JVM级别本地缓存Caffeine青出于蓝的要诀 —— 缘何会更强、如何去上手

【全网首发】一次疑似 JVM Native 内存泄露的问题分析

【全网首发】一次疑似 JVM Native 内存泄露的问题分析

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

【全网首发】从源码角度分析一次诡异的类被加载问题

【全网首发】从源码角度分析一次诡异的类被加载问题

7
1