深入JVM HotSpot C++源码:解析G1/ZGC并行/并发阶段的实现细节

好的,我们开始。

深入JVM HotSpot C++源码:解析G1/ZGC并行/并发阶段的实现细节

大家好,今天我们深入HotSpot JVM的C++源码,探讨G1和ZGC两种垃圾收集器在并行和并发阶段的具体实现细节。理解这些细节有助于我们更好地理解JVM的GC工作机制,从而进行更有效的性能调优。

G1的并行和并发阶段

G1 (Garbage-First) 垃圾收集器旨在取代CMS收集器,提供更可预测的停顿时间。它将堆划分为多个大小相等的Region,并优先回收垃圾最多的Region。G1主要分为以下几个阶段:

  1. Initial Mark(初始标记): 标记GC Roots直接可达的对象。这是一个STW(Stop-The-World)阶段。
  2. Concurrent Marking(并发标记): 从GC Roots开始,并发地遍历对象图,标记所有可达对象。
  3. Remark(重新标记): 完成并发标记阶段的剩余工作,处理并发标记期间对象的变化。这是一个STW阶段。
  4. Cleanup(清理): 计算Region的存活对象比例,并对Region进行排序,确定需要回收的Region。部分工作是并发的。
  5. Copying(复制/疏散): 将存活对象复制到新的Region,并回收旧Region。这是一个STW阶段。

接下来,我们通过源码来分析G1的并行和并发阶段。

1. 并发标记(Concurrent Marking)

并发标记阶段的核心逻辑在 G1ConcurrentMark 类中。 这个类继承自 ConcurrentGCThread

// in src/hotspot/share/gc/g1/g1ConcurrentMark.hpp

class G1ConcurrentMark : public ConcurrentGCThread {
 private:
  G1CollectedHeap* _g1h;
  G1CMTaskQueueSet* _cm_task_queue_set;
  // ... 省略其他成员变量

 public:
  G1ConcurrentMark(G1CollectedHeap* g1h, G1CMTaskQueueSet* cm_task_queue_set);
  virtual void run_service();

  void enqueue_roots_tasks();
  void enqueue_region_tasks();
  void process_task(G1CMTask* task);
  void process_strong_roots_task(G1CMStrongRootsTask* task);
  void process_region_task(G1CMRegionTask* task);
};

run_service() 方法是并发标记线程的主要入口点:

// in src/hotspot/share/gc/g1/g1ConcurrentMark.cpp

void G1ConcurrentMark::run_service() {
  //... 省略一些初始化代码

  // 1. Enqueue tasks for root regions (roots)
  enqueue_roots_tasks();

  // 2. Enqueue tasks for other regions
  enqueue_region_tasks();

  // 3. Process tasks until the queues are empty
  while (!_cm_task_queue_set->is_empty() || !should_terminate()) {
    G1CMTask* task = _cm_task_queue_set->dequeue();
    if (task != NULL) {
      process_task(task);
      task->retire(); // Return the task object to the free list
    } else {
      // No tasks available, yield the CPU
      os::yield();
    }
  }

  //... 省略一些清理代码
}

这段代码的关键在于:

  • enqueue_roots_tasks()enqueue_region_tasks():将需要扫描的Roots 和 Region 包装成Task,放入一个TaskQueue中,让多个线程并发处理。
  • _cm_task_queue_set->dequeue():从任务队列中获取任务。
  • process_task():处理任务,它会根据任务类型调用 process_strong_roots_task()process_region_task()

process_region_task() 负责扫描Region中的对象,并标记可达对象:

// in src/hotspot/share/gc/g1/g1ConcurrentMark.cpp

void G1ConcurrentMark::process_region_task(G1CMRegionTask* task) {
  HeapRegion* r = task->region();

  if (r->is_empty()) {
    // Region is empty, nothing to do
    return;
  }

  MemRegion mr = r->used_region();
  G1ParScanClosure cl(_g1h, _cm_task_queue_set); // 扫描闭包

  _g1h->iterate_mem_region(r, mr, &cl); // 遍历Region中的对象
}

G1ParScanClosure 是一个扫描闭包,用于处理扫描到的对象:

// in src/hotspot/share/gc/g1/g1ParScan.hpp

class G1ParScanClosure : public OopClosure {
 private:
  G1CollectedHeap* _g1h;
  G1CMTaskQueueSet* _cm_task_queue_set;

 public:
  G1ParScanClosure(G1CollectedHeap* g1h, G1CMTaskQueueSet* cm_task_queue_set) :
    _g1h(g1h), _cm_task_queue_set(cm_task_queue_set) {}

  virtual void do_oop(Oop* p);
};

do_oop() 方法是实际进行对象标记的地方:

// in src/hotspot/share/gc/g1/g1ParScan.cpp

void G1ParScanClosure::do_oop(Oop* p) {
  Oop obj = *p;
  if (!Universe::heap()->is_in_reserved(obj)) {
    // Not in the heap, ignore.
    return;
  }
  if (_g1h->is_obj_old(obj)) {
    if (!_g1h->is_marked_conc(obj)) {
      if (_g1h->mark_obj_conc(obj)) {
         // 对象成功标记,可能需要进一步扫描其引用的对象
         G1CMTask* task = _cm_task_queue_set->steal_or_new_task();
         task->set_region(_g1h->heap_region_containing(obj));
         _cm_task_queue_set->enqueue(task); // 将Region加入任务队列,以待后续扫描
      }
    }
  }
}

这里 _g1h->mark_obj_conc(obj) 尝试标记对象。如果对象成功标记,并且该对象位于一个Region中,则将包含该对象的Region添加到任务队列中,以便后续扫描该Region中其他对象的引用。

2. 并行复制(Parallel Copying/Evacuation)

并行复制阶段发生在STW期间,用于将存活对象从待回收的Region复制到新的Region。

// in src/hotspot/share/gc/g1/g1CollectedHeap.cpp

void G1CollectedHeap::do_collection_pause(double pause_time_sec) {
  //... 省略很多代码

  // 执行复制/疏散阶段
  g1_evacuate_collected_sets(gc_cause, errmsg_buf, errmsg_size);

  //... 省略很多代码
}

g1_evacuate_collected_sets() 函数负责执行复制/疏散操作:

// in src/hotspot/share/gc/g1/g1CollectedHeap.cpp

void G1CollectedHeap::g1_evacuate_collected_sets(GCCause::Cause gc_cause,
                                                 char* errmsg_buf,
                                                 size_t errmsg_size) {
  //... 省略一些代码

  G1EvacuateRegionsClosure evac_rs_cl(_g1h, &evac_stats);
  G1ConcurrentRefine::process_strong_roots(&evac_rs_cl);

  // 并行复制
  uint worker_count = ParallelGCThreads;
  G1EvacuateFollowersClosure evac_fl_cl(_g1h, &evac_stats);
  G1ParTaskGeneration par_gen(&evac_fl_cl, worker_count);
  par_gen.run(true);

  //... 省略一些代码
}

这段代码的关键在于:

  • G1EvacuateRegionsClosure:用于处理Root Region的复制。
  • G1EvacuateFollowersClosure:用于处理其他Region的复制。
  • G1ParTaskGeneration:创建一个并行任务生成器,用于并发地执行复制任务。

G1EvacuateFollowersClosuredo_oop() 方法是实际进行对象复制的地方:

// in src/hotspot/share/gc/g1/g1Evacuate.hpp

class G1EvacuateFollowersClosure : public OopClosure {
 private:
  G1CollectedHeap* _g1h;
  G1EvacuationStats* _evac_stats;
 public:
  G1EvacuateFollowersClosure(G1CollectedHeap* g1h, G1EvacuationStats* evac_stats) :
    _g1h(g1h), _evac_stats(evac_stats) {}

  virtual void do_oop(Oop* p);
};
// in src/hotspot/share/gc/g1/g1Evacuate.cpp

void G1EvacuateFollowersClosure::do_oop(Oop* p) {
  Oop obj = *p;

  if (_g1h->is_obj_old(obj)) { // 确保对象在堆中
    oop new_obj = _g1h->evacuate_oop(obj); // 疏散对象
    if (new_obj != obj) {
      *p = new_obj; // 更新引用
    }
  }
}

_g1h->evacuate_oop(obj) 负责将对象复制到新的Region,并返回新对象的地址。如果对象成功复制,则更新引用。

总的来说,G1的并行和并发阶段通过任务队列和多线程技术,实现了高效的垃圾回收。并发标记允许在应用程序运行的同时进行垃圾标记,从而减少停顿时间。并行复制则利用多核处理器的能力,加速对象的复制过程。

ZGC的并行和并发阶段

ZGC (Z Garbage Collector) 是一种低延迟的垃圾收集器,它几乎完全在并发状态下工作,停顿时间极短。ZGC的主要阶段包括:

  1. Concurrent Mark(并发标记): 从GC Roots开始,并发地遍历对象图,标记所有可达对象。
  2. Concurrent Relocate(并发转移): 将存活对象转移到新的内存区域,并更新所有指向这些对象的引用。
  3. Concurrent Reset(并发重置): 重置GC元数据,为下一次GC循环做准备。

ZGC的核心思想是着色指针(Colored Pointers)和读屏障(Read Barriers)。 着色指针在指针中嵌入了额外的信息,用于垃圾收集的各种操作。 读屏障则是在读取对象引用时执行的一段代码,用于更新引用、检测对象是否被转移等。

1. 并发标记(Concurrent Marking)

ZGC的并发标记阶段与G1类似,也是从GC Roots开始遍历对象图,标记可达对象。但ZGC的标记信息存储在着色指针中。

// in src/hotspot/share/gc/z/zMark.cpp

void ZMark::mark(ZThread* thread) {
  //... 省略一些初始化代码

  // Enqueue roots tasks
  enqueue_roots_tasks(thread);

  // Process tasks until done
  while (!try_process_or_wait(thread)) {
    // Loop until all tasks are processed
  }

  //... 省略一些清理代码
}

enqueue_roots_tasks()try_process_or_wait() 与G1类似,将需要扫描的Roots包装成Task,放入一个TaskQueue中,让多个线程并发处理。 try_process_or_wait 从队列中获取任务并处理。

ZGC的标记过程依赖于着色指针。 mark_object() 函数是标记对象的核心:

// in src/hotspot/share/gc/z/zMark.cpp

bool ZMark::mark_object(oop obj) {
  uintptr_t marked_value = ZAddress::mark(obj);

  if (marked_value != ZAddress::good_value(obj)) {
    // Already marked or other state, return false
    return false;
  }

  // Mark the object
  ZAddress::set_mark(obj);
  return true;
}

ZAddress::set_mark(obj) 使用位运算修改着色指针中的标记位:

// in src/hotspot/share/gc/z/zAddress.inline.hpp

inline void ZAddress::set_mark(oop obj) {
  uintptr_t addr = oop_addr(obj);
  uintptr_t value = LoadAcquire::uintptr_t(addr);
  uintptr_t new_value = value | ZAddress::MarkedMask;

  if (value != new_value) {
     OrderAccess::release_store((volatile uintptr_t*)addr, new_value);
  }
}

ZAddress::MarkedMask 是标记位的掩码。 通过设置指针中的标记位,ZGC实现了对象标记。

2. 并发转移(Concurrent Relocate)

并发转移阶段是ZGC的核心。它将存活对象转移到新的内存区域,并更新所有指向这些对象的引用。 这一阶段也依赖于着色指针和读屏障。

// in src/hotspot/share/gc/z/zRelocate.cpp

void ZRelocate::relocate(ZThread* thread) {
  // ... 省略初始化

  // Enqueue roots tasks
  enqueue_roots_tasks(thread);

  // Process tasks until done
  while (!try_process_or_wait(thread)) {
    // Loop until all tasks are processed
  }

  //... 省略清理
}

enqueue_roots_tasks()try_process_or_wait() 与标记阶段类似,负责任务的调度。

relocate_object() 函数是实际进行对象转移的地方:

// in src/hotspot/share/gc/z/zRelocate.cpp

oop ZRelocate::relocate_object(oop obj) {
  if (ZAddress::is_relocated(obj)) {
    // Already relocated, return the new address
    return ZAddress::decode_relocated(obj);
  }

  HeapRegion* hr = ZHeap::heap_region(obj);
  if(hr->is_free()){
    return obj;
  }

  oop new_obj = ZHeap::allocate(obj->size());

  if (new_obj == NULL) {
    return NULL;
  }

  // Copy the object to the new location
  Copy::disjoint_words((HeapWord*)obj, (HeapWord*)new_obj, obj->size());

  // Update the pointer to point to the new object
  ZAddress::encode_relocated(obj, new_obj);

  return new_obj;
}

这段代码的关键在于:

  • ZAddress::is_relocated(obj):检查对象是否已经被转移。
  • ZHeap::allocate(obj->size()):在新的位置分配内存。
  • Copy::disjoint_words((HeapWord*)obj, (HeapWord*)new_obj, obj->size()):将对象复制到新的位置。
  • ZAddress::encode_relocated(obj, new_obj):使用着色指针记录对象的转移信息。

ZAddress::encode_relocated(obj, new_obj) 函数使用着色指针将旧对象的地址指向新对象的地址,从而实现引用的更新:

// in src/hotspot/share/gc/z/zAddress.inline.hpp

inline void ZAddress::encode_relocated(oop obj, oop new_obj) {
  uintptr_t addr = oop_addr(obj);
  uintptr_t value = oop_addr(new_obj) | ZAddress::RelocatedMask;
  OrderAccess::release_store((volatile uintptr_t*)addr, value);
}

ZAddress::RelocatedMask 是转移位的掩码。 通过设置指针中的转移位,并将指针指向新对象的地址,ZGC实现了对象的转移和引用的更新。

在并发转移阶段,当线程尝试访问一个对象时,读屏障会拦截该访问,并检查对象是否已经被转移。如果对象已经被转移,读屏障会返回新对象的地址,从而保证线程访问的是最新的对象。

ZGC的读屏障实现如下:

// in src/hotspot/cpu/x86/zBarrierSetAssembler_x86.cpp

void ZBarrierSetAssembler::read_barrier_load(
    Assembler* a,
    Register dst,
    Address src,
    Register tmp1,
    Register tmp2,
    bool preserve_dst
) {
  // Load the reference from memory
  a->movptr(dst, src);

  // Check if the reference is relocated
  a->testptr(dst, ZAddress::RelocatedMask);
  a->jcc(Assembler::zero, done);

  // Slow path: call the runtime to update the reference
  a->push(dst);
  a->call(RuntimeAddress(ZReadBarrier::read_barrier_load_entry()));
  a->addptr(rsp, BytesPerWord);

done:
  a->end();
}

这段代码的关键在于:

  • a->testptr(dst, ZAddress::RelocatedMask):检查指针是否设置了转移位。
  • 如果设置了转移位,则调用 ZReadBarrier::read_barrier_load_entry() 进入慢速路径,更新引用。

总的来说,ZGC通过着色指针和读屏障,实现了完全并发的垃圾回收。并发标记和并发转移允许在应用程序运行的同时进行垃圾回收,从而实现了极低的停顿时间。

G1与ZGC并行/并发阶段的对比

为了更清晰地理解G1和ZGC在并行/并发阶段的差异,我们用表格进行对比:

特性 G1 ZGC
并发标记 并发标记线程扫描对象图,标记可达对象。 并发标记线程扫描对象图,使用着色指针标记可达对象。
并发转移 需要STW阶段进行并行复制。 完全并发,通过着色指针和读屏障实现对象的转移和引用的更新。
并行性/并发性 部分并发(并发标记),部分并行(并行复制)。 几乎完全并发。
停顿时间 可配置目标停顿时间,但通常比ZGC长。 极低,通常在几毫秒以内。
内存管理 将堆划分为多个Region,优先回收垃圾最多的Region。 将堆划分为多个Page,使用动态Region大小。
读屏障 无。 有,用于拦截对象访问,并更新引用。
着色指针 无。 有,用于记录对象的标记和转移信息。
适用场景 适用于需要可预测停顿时间,但对停顿时间要求不苛刻的场景。 适用于对停顿时间要求极高的场景。

两者的源码实现的关键差异

  • 对象标记方式: G1使用传统的bitmap或者Remembered Set来记录对象的标记信息,而ZGC则直接在对象指针上进行标记(着色指针)。
  • 对象转移机制: G1的对象转移需要STW暂停,然后进行并行复制,而ZGC则是完全并发的,依赖于读屏障来保证在对象转移过程中的访问正确性。
  • 并发程度: ZGC的并发程度更高,几乎所有的GC阶段都是并发执行的,而G1则需要在某些阶段暂停应用。

总结

G1和ZGC是HotSpot JVM中两种重要的垃圾收集器,它们在并行和并发阶段的实现细节各有不同。G1通过Region划分和并行复制,实现了可预测的停顿时间。ZGC则通过着色指针和读屏障,实现了极低的停顿时间。 理解这些细节有助于我们根据不同的应用场景选择合适的垃圾收集器,并进行更有效的性能调优。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注