OpenJDK16 ZGC 源码分析(四)GC阶段之初始标记

1. 简介

GC回收周期大体如下图所示:
在这里插入图片描述

与早期版本不同,ZGC回收周期调整为9个子阶段:
phase 1:初始标记,需要STW
phase 2:并发标记
phase 3:标记结束,需要STW
phase 4:并发处理软引用、弱引用
phase 5:并发重置Relocation Set
phase 6:验证
phase 7:并发选择Relocation Set
phase 8:开始Relocate,STW
phase 9:并发Relocate
出于回收效率的考虑,remap过程放在下一个回收周期的并发标记子阶段进行。
在这里插入图片描述

本文将详细介绍初始标记和并发标记阶段。

2. 源码分析

2.1 GC步骤

zDriver是ZGC的驱动类,GC由这儿触发。
下面的代码可以清晰看出所有GC步骤。
ZDriver.cpp

void ZDriver::gc(GCCause::Cause cause) {
  ZDriverGCScope scope(cause);

  // 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 Process Non-Strong References
  concurrent_process_non_strong_references();

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

  // Phase 6: Pause Verify
  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();
}

2.2 初始标记入口

初始标记需要STW,具体逻辑由VMThread线程执行。
VMThread是JVM执行垃圾回收的同步线程, 是JVM最重要的线程之一,主要作用就是处理垃圾回收。
ZDriver.cpp

void ZDriver::pause_mark_start() {
  pause<VM_ZMarkStart>();
}

template <typename T>
bool ZDriver::pause() {
  for (;;) {
    T op;
    // 将VM_ZMarkStart放入队列,等待VMThread的run方法轮询拉取任务
    VMThread::execute(&op);
    if (op.gc_locked()) {
      ZStatTimer timer(ZCriticalPhaseGCLockerStall);
      _gc_locker_port.wait();
      continue;
    }

    // Notify VM operation completed
    _gc_locker_port.ack();

    return op.success();
  }
}

VM_ZMarkStart是初始标记的具体实现,初始标记主要分如下步骤:

  • 设置软引用回收策略
  • 设置是否进入加强回收模式
  • 调用ZHeap的初始标记函数
    ZDriver.cpp
class VM_ZMarkStart : public VM_ZOperation {
public:
  virtual VMOp_Type type() const {
    return VMOp_ZMarkStart;
  }

  virtual bool needs_inactive_gc_locker() const {
    return true;
  }

  virtual bool do_operation() {
    ZStatTimer timer(ZPhasePauseMarkStart);
    ZServiceabilityPauseTracer tracer;

    // 软引用回收策略
    const bool clear = should_clear_soft_references();
    ZHeap::heap()->set_soft_reference_policy(clear);

    // 设置加强回收模式
    const bool boost = should_boost_worker_threads();
    ZHeap::heap()->set_boost_worker_threads(boost);

    ZCollectedHeap::heap()->increment_total_collections(true /* full */);

    ZHeap::heap()->mark_start();
    return true;
  }
};

是否回收软引用判断逻辑如下:
ZDriver.cpp

static bool should_clear_soft_references() {
  // 存在阻塞内存分配请求堆积
  const bool stalled = ZHeap::heap()->is_alloc_stalled();
  if (stalled) {
    // 清理软引用
    return true;
  }

  // 判断GC原因
  // 如果是full gc或者元空间不足触发的gc,则回收软引用
  const GCCause::Cause cause = ZCollectedHeap::heap()->gc_cause();
  if (cause == GCCause::_wb_full_gc ||
      cause == GCCause::_metadata_GC_clear_soft_refs) {
    // Clear
    return true;
  }

  // Don't clear
  return false;
}

这儿简单介绍一下几种常见的 GCCause。
gcCause.hpp

class GCCause : public AllStatic {
 public:
  enum Cause {
    /* public */
    // System.gc()触发的GC, ZGC不处理
    _java_lang_system_gc,
    _full_gc_alot,
    _scavenge_alot,
    _allocation_profiler,
    // 通过jvmti方式触发的GC
    _jvmti_force_gc,
    _gc_locker,
    _heap_inspection,
    // jmap dump内存前/后,触发的GC
    _heap_dump,
    // wb开头的,都是通过WhiteBox API触发的
    _wb_young_gc,
    _wb_conc_mark,
    _wb_full_gc,
    _wb_breakpoint,
    _archive_time_gc,

    /* implementation independent, but reserved for GC use */
    _no_gc,
    _no_cause_specified,
    // 常见的GC cause,分配对象失败
    _allocation_failure,

    /* implementation specific */

    _tenured_generation_full,
    // 元空间分配失败
    _metadata_GC_threshold,
    // 上一次GC后,空间仍然不足,再触发一次回收软引用的GC
    _metadata_GC_clear_soft_refs,

    _old_generation_expanded_on_last_scavenge,
    _old_generation_too_full_to_scavenge,
    _adaptive_size_policy,
    
    // 以下为G1特有的GC cause
    // G1,对象分配失败
    _g1_inc_collection_pause,
    // G1,大对象分配失败
    _g1_humongous_allocation,
    // JDK 12引入的G1新特性触发的G1定时回收
    // JEP 346 http://openjdk.java.net/jeps/346
    _g1_periodic_collection,

    _dcmd_gc_run,

    // 以下为shenandoahGC特有的GC cause
    _shenandoah_stop_vm,
    _shenandoah_allocation_failure_evac,
    _shenandoah_concurrent_gc,
    _shenandoah_upgrade_to_full_gc,

    // 以下为ZGC特有的GC cause
    // 定时
    _z_timer,
    // 预热机制启动
    _z_warmup,
    // 分配率阈值
    _z_allocation_rate,
    // 阻塞内存分配
    _z_allocation_stall,
    // 主动触发
    _z_proactive,
    // 内存高使用率
    _z_high_usage,

    _last_gc_cause
  };
}

是否增强回收模式逻辑如下:
ZDriver.cpp

static bool should_boost_worker_threads() {
  // 存在阻塞内存分配请求堆积
  const bool stalled = ZHeap::heap()->is_alloc_stalled();
  if (stalled) {
    // 启动增强模式
    return true;
  }

  // Boost worker threads if implied by the GC cause
  const GCCause::Cause cause = ZCollectedHeap::heap()->gc_cause();
  if (cause == GCCause::_wb_full_gc ||
      cause == GCCause::_java_lang_system_gc ||
      cause == GCCause::_metadata_GC_clear_soft_refs) {
    // full gc
    // java代码执行System.gc();
    // 元空间不足
    // 启动增强模式
    return true;
  }

  // Don't boost
  return false;
}
  • 增强回收模式,其实就是内存快要耗尽时,ZGC启动更多工作线程参与回收。

2.3 初始标记

zHeap.cpp

void ZHeap::mark_start() {
  // 必须在安全点执行
  assert(SafepointSynchronize::is_at_safepoint(), "Should be at safepoint");

  // Flip address view
  flip_to_marked();

  // Retire allocating pages
  _object_allocator.retire_pages();

  // 重置统计数据
  _page_allocator.reset_statistics();

  // 重置统计数据
  _reference_processor.reset_statistics();

  // 修改全局变量,标记GC阶段为mark phase
  ZGlobalPhase = ZPhaseMark;

  // Reset marking information and mark roots
  _mark.start();

  // Update statistics
  ZStatHeap::set_at_mark_start(_page_allocator.stats());
}

2.3.1 安全点

STW操作需要在Java线程全部进入安全点后执行,主要目的是获取稳定的线程堆栈信息。
如下列代码所示,对于VM Thread仅需一个简单的状态判断即可。
runtime/safepoint.hpp

  enum SynchronizeState {
      _not_synchronized = 0,                   // Java线程没有处于安全点
      _synchronizing    = 1,                   // Java线程正在同步中
      _synchronized     = 2                    // 所有Java线程已经处于运行native代码、被OS阻塞或停止在安全点,VM Thread、非Java线程可以运行
  };

static bool is_at_safepoint()                   { return _state == _synchronized; }

而Java线程在执行过程中,需要不停的检查是否需要停留在安全点。解释执行时,字节码方法返回时、回边时、JNI调用结束后进行判断;而编译执行时,C1/C2等即时编译器则需要织入安全点检查代码。
share/interpreter/zero/bytecodeInterpreter.cpp

      CASE(_fcmpl):
      CASE(_fcmpg):
      // 删除无关代码

      CASE(_dcmpl):
      CASE(_dcmpg):
      // 删除无关代码

      CASE(_lcmp):
      // 删除无关代码

      /* Return from a method */

      CASE(_areturn):
      CASE(_ireturn):
      CASE(_freturn):
      {
          SAFEPOINT;

          goto handle_return;
      }

      CASE(_lreturn):
      CASE(_dreturn):
      {
          SAFEPOINT;
          goto handle_return;
      }

      CASE(_return_register_finalizer): {
          // _return_register_finalizer是JVM自定义的字节码,仅用于构造重写了finalized方法的对象
          goto handle_return;
      }
      CASE(_return): {
		  // 安全点检查
          SAFEPOINT;
          goto handle_return;
      }

2.3.2 视图切换

确认进入安全点后,初始标记的第一步就是切换colored指针视图。
share/gc/z/zHeap.cpp
share/gc/z/zAddress.cpp

void ZHeap::flip_to_marked() {
  ZVerifyViewsFlip flip(&_page_allocator);
  ZAddress::flip_to_marked();
}

void ZAddress::flip_to_marked() {
  ZAddressMetadataMarked ^= (ZAddressMetadataMarked0 | ZAddressMetadataMarked1);
  set_good_mask(ZAddressMetadataMarked);
}
  • 如上述代码所示,切换视图其实就是从remap切换到m0或者m1,并更新Good掩码

2.3.3 回收Page

retire_pages主要是重置了统计数据和共享页面。_shared_medium_page和_shared_small_page的作用可以参考OpenJDK16 ZGC 源码分析(二)对象分配
share/gc/z/zObjectAllocator.cpp

void ZObjectAllocator::retire_pages() {
  // 必须在安全点执行
  assert(SafepointSynchronize::is_at_safepoint(), "Should be at safepoint");

  // 重置统计数据,重置used和undone字节数
  _used.set_all(0);
  _undone.set_all(0);

  // 重置medium page和small page共享页面
  _shared_medium_page.set(NULL);
  _shared_small_page.set_all(NULL);
}

2.3.4 启动

share/gc/z/zMark.cpp

void ZMark::start() {
  // 验证标记栈和标记条带为null
  if (ZVerifyMarking) {
    verify_all_stacks_empty();
  }

  // 更新GC纪元
  ZGlobalSeqNum++;

  // 重置计数器
  _nproactiveflush = 0;
  _nterminateflush = 0;
  _ntrycomplete = 0;
  _ncontinue = 0;

  // 设置工作线程数
  _nworkers = _workers->nconcurrent();

  // 计算标记条带数量,标记条带数量必须是2的n次幂
  const size_t nstripes = calculate_nstripes(_nworkers);
  _stripes.set_nstripes(nstripes);

  ZStatMark::set_at_mark_start(nstripes);

  // 略去统计代码
}

如下代码计算初始标记线程数,如果是增强模式,取ParallelGCThreads的值,否则取ConcGCThreads的值。
在ZGC中ParallelGCThreads的默认值是ceil(可用核数60%),而ConcGCThreads的默认值是ceil(可用核数12.5%)
share/gc/z/zWorkers.inline.cpp
share/gc/z/zHeuristics.cpp

inline uint ZWorkers::nconcurrent() const {
  return _boost ? nworkers() : nconcurrent_no_boost();
}

inline uint ZWorkers::nconcurrent_no_boost() const {
  return ConcGCThreads;
}

inline uint ZWorkers::nworkers() const {
  return MAX2(ParallelGCThreads, ConcGCThreads);
}

uint ZHeuristics::nparallel_workers() {
  return nworkers(60.0);
}

uint ZHeuristics::nconcurrent_workers() {
  return nworkers(12.5);
}

3. 总结

本文介绍了ZGC的9个主要阶段,并重点介绍了初始标记的步骤。其他阶段将在后文中陆续介绍。
初始标记阶段需要等待进入全局安全点并STW,标记GC roots,该阶段不随heap size的增长线性增长,STW时间可控 。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值