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时间可控 。