在JVM G1源码分析——新生代回收介绍新生代回收的时候我们提到,新生代回收会收集所有的YHR;而本章介绍的混合回收(Mixed GC,也称为混合GC)既收集YHR也收集OHR。因为涉及老生代的回收,通常来说老生代的空间比较大,收集老生代可能会花费更多的时间。所以涉及老生代的混合收集算法也不同于新生代回收算法,最明显的是引入并发标记,这里的并发标记指的是标记工作线程可以和Mutator同时运行,当然并发标记引入了复杂度。混合回收可以总结为两个阶段:
- 并发标记,目的是识别老生代分区中的活跃对象,并计算分区中垃圾对象所占空间的多少,用于垃圾回收过程中判断是否回收分区。
- 垃圾回收,这个过程和新生代回收的步骤完全一致,重用了新生代回收的代码,最大的不同是在回收时不仅仅回收新生代分区,同时回收并发标记中识别到的垃圾多的老生代分区。
本章主要介绍混合回收中用到的并发标记算法,同时解释了并发标记算法的难点,G1中混合回收的步骤以及混合回收中并发标记算法,着重介绍了算法中的并发标记子阶段、Remark(再标记)子阶段和清理子阶段,并分析了相关的代码;演示了并发标记算法每一步所做的工作,总结了G1中YGC和混合回收整体活动图;最后介绍了如何解读日志和参数调优。
注意:在有些文档中混合回收仅仅指第二阶段即垃圾回收过程,并不包含并发标记过程;在另一些文档中并发标记指的是并发标记过程中的并发标记子阶段。
并发标记算法详解
并发标记算法是混合回收中最重要的算法。并发标记指的是标记线程和Mutator并发运行。那么标记线程如何并发地进行标记?正如我们前面提到的并发标记的难点,一边标记垃圾对象,一边还在生成垃圾对象。为了解决这个问题,以前的算法采用串行执行,这里的串行指的是标记工作和对象生成工作不同时进行,但在G1中引入了新的算法。在介绍并发标记算法之前我们首先回顾一下对象分配,再来讨论这个问题。前面我们提到在堆分区中分配对象的时候,对象都是连续分配。在介绍TLAB时提到为了效率对还没有填充满的TLAB填充一个dummy的Int[]之类。所以可以设计几个指针分别是Bottom、Prev、Next和Top,用Prev指针指向上一次并发处理后的地址,用Next指向并发标记开始之前内存已经分配成功的地址,当并发标记开始之后,如果有新的对象分配,可以移动Top指针,使Top指针指向当前内存分配成功的地址。Next指针和Top指针之间的地址就是Mutator新增的对象使用的地址。如果我们假设Prev指针之前的对象已经标记成功,在并发标记的时候从根出发,不仅仅标记Prev和Next之间的对象,还标记了Prev指针之前活跃的对象。当并发标记结束之后,只需要把Prev指针设置为Next指针即可开始新一轮的标记处理。
Prev和Next指针解决了并发标记工作内存区域的问题,还需要在引入两个额外的数据结构来记录内存标记的状态,典型的是使用位图来指示哪块内存已经使用,哪块内存还未使用。所以并发标记引入两个位图PrevBitMap和NextBitMap,用PrevBitmap记录prev指针之前内存的标记状况,用NextBitmap来表示整个内存到next指针之前的标记状态。
这里很多人都很奇怪NextBitmap包含了整个使用内存的标记状态,为什么要引入PrevBitmap这个数据结构?这个数据结构在什么时候使用?我们可以想象如果并发标记每次都成功,我们确实可以不需要PrevBitmap,只需要根据这个BitMap对对象进行清除即可。但是如果发生标记失败将会发生什么?我们将丢失上一次对Prev指针之前所有内存的标记状况,也就是说当发生失败不能完成并发标记时将需要重新标记整个内存,这显然是不对的。
并发标记开始之前如上图所示,这里用Bottom表示分区的底部,Top表示分区空间使用的顶部,TAMS指的是Top-at-Mark-Start,Prev就是前一次标记的地址即Prev TAMS,Next指向的是当前开始标记时最新的地址即Next TAMS。并发标记会从根对象出发开始进行并发标记。在第一次标记时PrevBitmap为空,NextBitmap待标记。
并发标记结束后,NextBitmap标记了分区对象存活的情况,如下图所示。
假定上图的位图中黑色区域表示堆分区中对应的对象还活着。在并发标记的同时Mutator继续运行,所以Top会继续增长。
新一轮的并发标记开始,交换位图,重置指针。如下图所示:
并发标记算法的难点
并发标记的主要问题是垃圾回收器在标记对象的过程中,Mutator可能正在改变对象引用关系图,从而造成漏标和错标。错标不会影响程序的正确性,只是造成所谓的浮动垃圾。但漏标则会导致可达对象被当做垃圾收集掉,从而影响程序的正确性。为了区别对象的不同状态,引入了三色标记法。
三色标记法
三色标记法是一个逻辑上的抽象,将对象分成白色(white),表示还没有被收集器标记的对象;灰色(gray),表示自身已经被标记到,但其拥有的field字段引用到的其他对象还没有被处理;黑色(black),表示自身已经被标记到,且对象本身所有的field引用到的对象也已经被标记。对象在并发标记阶段会被漏标的充分必要条件是:
- Mutator插入了一个从黑色对象到该白色对象的新引用,因为黑色对象已经被标记,如果不对黑色对象重新处理,那么白色对象将被漏标,造成错误。
- Mutator删除了所有从灰色对象到该白色对象的直接或者间接引用,因为灰色对象正在标记,字段引用的对象还没有被标记,如果这个引用的白色对象被删除了(引用发生了变化),那么这个引用对象也有可能被漏标。
因此,要避免对象的漏标,只需要打破上述两个条件中的任何一个即可[插图]。所以在并发标记的时候也对应地有两种不同的实现:
- 增量更新算法关注对象引用插入,把被更新的黑色或者白色对象标记成灰色,打破第一个条件。
- SATB关注引用的删除,即在对象被赋值前,把老的被引用对象记录下来,然后根据这些对象为根重新标记一遍,打破第二个条件。
难点示意图
为了直观地理解并发标记的难点,用一个例子来演示,如下图所示:
图中有4个对象,3种颜色分别是黑色,灰色和白色。我们仅仅关注图中对象1、对象2和对象3。其中对象1和对象2都可以通过根对象到达。假定对象1已经被标记,所以设置为黑色。处理完对象1会把对象1的field指向的对象地址放入到待标记栈。当对象2已经标记完成,需要把对象2的field指向的对象栈,即对象3入栈待处理。如果此时并发标记线程让出CPU,Mutator执行并修改了引用关系。对象2.field=NULL,对象1.field=对象3,如下图所示:
这时候并发线程重新获得执行,将会发生什么?对象1已经变成黑色,说明field都标记完了。对象2灰色待处理field,但是field已经为NULL,所以不需要处理。那么对象3怎么办?如果不进行额外的处理就会导致漏标。这里就需要上面提到的两种解决漏标的方法。第一个就是增量更新,它的思路就是当发生了对象1.field=对象3,就把对象1重新标记为灰色,意味着对象1的field需要被再次处理一遍,如下图所示:
第二种方法就是SATB,这个思路就是在发生对象2.field=NULL之前把老的对象2.field指向的对象3放入待标记栈中,相当于把对象3设置成灰色,如下图所示:
再谈写屏障
在介绍RSet的时候就涉及了写屏障,这里也会有写屏障。RSet中写屏障的主要目的是为了标记引用关系,STAB中写屏障主要是为了保证并行标记的正确性,STAB主要记录的是目的对象修改之前的对象。我们从一段Java代码出发,看看JVM如何处理写屏障。
假定有一个TestExample类,里面有一个字符,并且对这个字符赋值,如下所示。
public class TestExample {
private Object obj;
public TestExample() {
obj = new Object();
}
}
这段Java代码会被编译成字节码(Bytecode),关于字节码的知识可以参考其他书籍或者文章。我们这里直接看一下这段代码对应的字节码,其中的成员变量赋值会被翻译成putField。对应字节码如下:
public class TestExample {
// 反编译文件: TestExample.java
// 访问控制符0x2标识private
private Ljava/lang/Object; obj
// 访问控制符0x1标识public
public <init>()V
L0
LINENUMBER 6 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 7 L1
ALOAD 0
NEW java/lang/Object
DUP
INVOKESPECIAL java/lang/Object.<init> ()V
PUTFIELD TestExample.obj : Ljava/lang/Object;
L2
LINENUMBER 8 L2
RETURN
L3
LOCALVARIABLE this LTestExample; L0 L3 0
MAXSTACK = 3
MAXLOCALS = 1
}
Putfield这个字节码是怎么实现的?早期的JVM使用的是字节解释器(BytecodeInterpreter),后来使用模板解释器(TemplateTable)。它们的功能是一样的,只不过实现的方式不同,字节解释器还需要再次解释执行到目标机器的代码,而模板解释器是针对平台的,JVM内部使用的是模板解释器,它是汇编语言编写的,为了简单起见,我们先看字节码解释器的代码。代码如下所示:
hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp
CASE(_putfield):
CASE(_putstatic):
{
…
obj->obj_field_put(field_offset, STACK_OBJECT(-1));
…
这两个字节码会找到对象,并调用obj_put_field,而obj_put_Field会调用oop_store,代码如下所示:
hotspot/src/share/vm/oops/oop.inline.hpp
inline void oopDesc::obj_field_put(int offset, oop value) {
UseCompressedOops oop_store(obj_field_addr<narrowOop>(offset), value) :
oop_store(obj_field_addr<oop>(offset), value);
}
真正的写屏障在oop_store中,代码如下所示:
template <class T> inline void oop_store(T* p, oop v) {
if (always_do_update_barrier) {
oop_store((volatile T*)p, v);
} else {
// 赋值前处理
update_barrier_set_pre((T*)p, v);
// 赋值动作
oopDesc::release_encode_store_heap_oop(p, v);
// 赋值后处理,注意这里使用的是(*)P,表示取p指向的对象,即new obj源对象
update_barrier_set((void*)p, v, true /* release */); // cast away type
}
}
hotspot/src/share/vm/oops/oop.inline.hpp
template <class T> inline void oop_store(volatile T* p, oop v) {
// 赋值前处理
update_barrier_set_pre((T*)p, v);
// 赋值动作
oopDesc::release_encode_store_heap_oop(p, v);
// 赋值后处理,注意这里使用的是(*)P,表示取p指向的对象,即new obj源对象
update_barrier_set((void*)p, v, true /* release */); // cast away type
}
inline void update_barrier_set(void* p, oop v, bool release = false) {
assert(oopDesc::bs() != NULL, "Uninitialized bs in oop!");
oopDesc::bs()->write_ref_field(p, v, release);
}
template <class T> inline void update_barrier_set_pre(T* p, oop v) {
oopDesc::bs()->write_ref_field_pre(p, v);
}
由于使用的垃圾收集器是G1,所以BarrierSet是G1SATBCardTableLoggingModRefBS类型,定义如下:
src\share\vm\memory\cardTableRS.cpp
CardTableRS::CardTableRS(MemRegion whole_heap,
int max_covered_regions) :
GenRemSet(),
_cur_youngergen_card_val(youngergenP1_card),
_regions_to_iterate(max_covered_regions - 1)
{
#if INCLUDE_ALL_GCS // 定义为1,包含所有的垃圾收集器
if (UseG1GC) {
// 启用G1垃圾收集器时用G1SATBCardTableLoggingModRefBS
_ct_bs = new G1SATBCardTableLoggingModRefBS(whole_heap,
max_covered_regions);
} else {
// 如果不是G1垃圾收集器时用CardTableModRefBSForCTRS
_ct_bs = new CardTableModRefBSForCTRS(whole_heap, max_covered_regions);
}
#else
_ct_bs = new CardTableModRefBSForCTRS(whole_heap, max_covered_regions);
#endif
_ct_bs->initialize(); // 初始化操作
set_bs(_ct_bs); // 设置BarrierSet
_last_cur_val_in_gen = NEW_C_HEAP_ARRAY3(jbyte, GenCollectedHeap::max_gens + 1,
mtGC, CURRENT_PC, AllocFailStrategy::RETURN_NULL);
if (_last_cur_val_in_gen == NULL) {
vm_exit_during_initialization("Could not create last_cur_val_in_gen array.");
}
for (int i = 0; i < GenCollectedHeap::max_gens + 1; i++) {
_last_cur_val_in_gen[i] = clean_card_val();
}
_ct_bs->set_CTRS(this);
}
write_ref_field和write_ref_field_pre的代码逻辑如下:
// Inline functions of BarrierSet, which de-virtualize certain
// performance-critical calls when the barrier is the most common
// card-table kind.
template <class T> void BarrierSet::write_ref_field_pre(T* field, oop new_val) {
if (kind() == CardTableModRef) {
((CardTableModRefBS*)this)->inline_write_ref_field_pre(field, new_val);
} else {
write_ref_field_pre_work(field, new_val);
}
}
void BarrierSet::write_ref_field(void* field, oop new_val, bool release) {
if (kind() == CardTableModRef) {
((CardTableModRefBS*)this)->inline_write_ref_field(field, new_val, release);
} else {
write_ref_field_work(field, new_val, release);
}
}
其中G1SATBCardTableModRefBS构造时,初始化_kind为G1SATBCT,所以上述代码会执行write_ref_field_pre_work和write_ref_field_work,则相关代码如下:
src\share\vm\gc_implementation\g1\g1SATBCardTableModRefBS.hpp
// These are the more general virtual versions.
virtual void write_ref_field_pre_work(oop* field, oop new_val) {
inline_write_ref_field_pre(field, new_val);
}
virtual void write_ref_field_pre_work(narrowOop* field, oop new_val) {
inline_write_ref_field_pre(field, new_val);
}
// We export this to make it available in cases where the static
// type of the barrier set is known. Note that it is non-virtual.
template <class T> inline void inline_write_ref_field_pre(T* field, oop newVal) {
write_ref_field_pre_static(field, newVal);
}
// This notes that we don't need to access any BarrierSet data
// structures, so this can be called from a static context.
template <class T> static void write_ref_field_pre_static(T* field, oop newVal) {
T heap_oop = oopDesc::load_heap_oop(field);
if (!oopDesc::is_null(heap_oop)) {
enqueue(oopDesc::decode_heap_oop(heap_oop));
}
}
可以总结为:
JVM ----> Insert Pre-write barrier
Object.Field = other_object; 真正的代码
JVM ----> Insert Post-write Barrier
赋值前处理会调用G1SATBCardTableModRefBS:inline_write_ref_field_pre,这是一个模板方法,最终就是把要写的目标对象放入到STAB的队列中,代码如下所示:
src\share\vm\gc_implementation\g1\g1SATBCardTableModRefBS.cpp
void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
if (!JavaThread::satb_mark_queue_set().is_active()) return;
Thread* thr = Thread::current();
// 对于一般的Mutator直接放入到线程的队列中。
if (thr->is_Java_thread()) {
JavaThread* jt = (JavaThread*)thr;
jt->satb_mark_queue().enqueue(pre_val);
} else {
// 对于本地代码则放入到全局共享队列中,因为是全局共享队列所以需要锁
MutexLockerEx x(Shared_SATB_Q_lock, Mutex::_no_safepoint_check_flag);
JavaThread::satb_mark_queue_set().shared_satb_queue()->enqueue(pre_val);
}
}
G1中为了代码阅读和理解的一致性,这里把赋值前处理也称为“写屏障”。其实更为准确的称呼应该是读屏障,把赋值前的对象先读出来进行标记处理,所以称为读屏障更为合适。为什么强调这个概念,就是希望大家能更加清晰地理解并发标记的原理。比如在ZGC中我们经常看到Load Barrier,也就是所谓的读屏障,指的是什么?也是在读对象的时候对对象做额外的处理,由此可以推断出ZGC中的并发标记算法应该和G1中的并发标记使用了同一算法。当然ZGC中因为对内存做了额外的设计,所以在具体实现的时候与G1有所不同。
赋值处理很简单,直接写对象地址到目标地址。赋值后处理主要是通过G1SATBCardTableLoggingModRefBS::write_ref_field_work完成,把源对象放入到dirty card队列,代码如下所示:
src\share\vm\gc_implementation\g1\g1SATBCardTableModRefBS.cpp
void write_ref_field_work(void* field, oop new_val, bool release) {
// 这里是源对象的地址
volatile jbyte* byte = byte_for(field);
// 如果源对象是新生代则不处理,因为不需要记录到新生代的引用,新生代不管是在哪种回收中都
// 会处理,所以不需要额外的记录。
if (*byte == g1_young_gen) {
return;
}
// 这里调用的storeload目的是为了保持数据的可见性
OrderAccess::storeload();
if (*byte != dirty_card) {
*byte = dirty_card;
Thread* thr = Thread::current();
// 对于一般的Mutator直接放入到线程的队列中。
if (thr->is_Java_thread()) {
JavaThread* jt = (JavaThread*)thr;
jt->dirty_card_queue().enqueue(byte);
} else {
// 对于本地代码则放入到全局共享队列中,因为是全局共享队列所以需要锁
MutexLockerEx x(Shared_DirtyCardQ_lock,
Mutex::_no_safepoint_check_flag);
_dcqs.shared_dirty_card_queue()->enqueue(byte);
}
}
}
实际上模板解释器代码也很清晰,putField会调用putfield_or_static,它会调用汇编的do_oop_store,代码如下所示:
hotspot/src/cpu/x86/vm/templateTable_x86_32.cpp
void TemplateTable::putfield_or_static(int byte_no, bool is_static) {
……
// atos
{
__ pop(atos);
if (!is_static) pop_and_check_object(obj);
// Store into the field
do_oop_store(_masm, field, rax, _bs->kind(), false);
if (!is_static) {
patch_bytecode(Bytecodes::_fast_aputfield, bc, rbx, true, byte_no);
}
__ jmp(Done);
}
……
}
这里do_oop_store和上面的oop_store功能类似,都是处理写前屏障、写动作和写后屏障,代码如下:
hotspot/src/cpu/x86/vm/templateTable_x86_32.cpp
static void do_oop_store(InterpreterMacroAssembler* _masm,
Address obj,
Register val,
BarrierSet::Name barrier,
bool precise) {
switch (barrier) {
#if INCLUDE_ALL_GCS
case BarrierSet::G1SATBCT:
case BarrierSet::G1SATBCTLogging:
{
……
// 这个就是处理SATB
__ g1_write_barrier_pre(rdx /* obj */,
rbx /* pre_val */,
rcx /* thread */,
rsi /* tmp */,
val != noreg /* tosca_live */,
false /* expand_call */);
if (val == noreg) {
// 赋值,对于赋值为空的对象不需要DCQ
__ movptr(Address(rdx, 0), NULL_WORD);
// No post barrier for NULL
} else {
// 赋值
__ movl(Address(rdx, 0), val);
// 处理DCQ
__ g1_write_barrier_post(rdx /* store_adr */,
val /* new_val */,
rcx /* thread */,
rbx /* tmp */,
rsi /* tmp2 */);
}
……
}
break;
#endif // INCLUDE_ALL_GCS
}
}
g1_write_barrier_pre和g1_write_barrier_post通过汇编调用函数g1_wb_pre和g1_wb_post,它们的作用是把对象放入SATB和DCQ中。
G1中混合回收的步骤
混合回收分为两个阶段:并发标记和垃圾回收,其中并发标记阶段可以分为:初始标记子阶段,并发标记子阶段,再标记子阶段和清理子阶段。垃圾回收阶段一定发生在并发标记阶段之后。
第一阶段:并发标记
- 初始标记子阶段
- 并发标记子阶段
- 再标记子阶段
- 清理子阶段
第二阶段:垃圾回收
本节按照混合回收发生的逻辑顺序依次来介绍这些内容,并在最后分析了并发标记算法的正确性。
1.初始标记子阶段
负责标记所有直接可达的根对象(栈对象、全局对象、JNI对象等),根是对象图的起点,因此初始标记需要将Mutator线程暂停,也就是需要一个STW的时间段。混合收集中的初始标记和新生代的初始标记几乎一样。实际上混合收集的初始标记是借用了新生代收集的结果,即新生代垃圾回收后的新生代Survivor分区作为根,所以混合收集一定发生在新生代回收之后,且不需要再进行一次初始标记。这个阶段在YGC中已经介绍,不再赘述。
2.并发标记子阶段
当YGC执行结束之后,如果发现满足并发标记的条件,并发线程就开始进行并发标记。根据新生代的Survivor分区以及老生代的RSet开始并发标记。并发标记的时机是在YGC后,只有达到InitiatingHeapOccupancyPercent阈值后,才会触发并发标记。InitiatingHeapOccupancyPercent默认值是45,表示的是当已经分配的内存加上即将分配的内存超过内存总容量的45%就可以开始并发标记。并发标记线程在并发标记阶段启动,由参数-XX:ConcGCThreads(默认GC线程数的1/4,即-XX:ParallelGCThreads/4)控制启动数量,每个线程每次只扫描一个分区,从而标记出存活对象。在标记的时候还会计算存活的数量(Live Data Accounting),只要一个对象被标记,同时会计算字节数,并计入分区空间,这和并发算法相关。
并发标记会对所有的分区进行标记。这个阶段并不需要STW,故标记线程和Mutator并发运行。
3.再标记子阶段
再标记(Remark)是最后一个标记阶段。在该阶段中,G1需要一个暂停的时间,找出所有未被访问的存活对象,同时完成存活内存数据计算。引入该阶段的目的,是为了能够达到结束标记的目标。要结束标记的过程,要满足三个条件:
- 从根(Survivor)出发,并发标记子阶段已经追踪了所有的存活对象。
- 标记栈是空的。
- 所有的引用变更都被处理了;这里的引用变更包括新增空间分配和引用变更,新增的空间所有对象都认为是活的,引用变更处理SATB。
前两个条件是很容易达到的,但是最后一个是很困难的。如果不引入一个STW的再标记过程,那么应用会不断地更新引用,也就是说,会不断产生新的引用变更,因而永远也无法达成完成标记的条件。这个阶段也是并行执行的,通过参数-XX:ParallelGCThreads可设置GC暂停时可用的GC线程数。同时,引用处理也是再标记阶段的一部分,所有重度使用引用对象(弱引用、软引用、虚引用、最终引用)的应用都需要不少的开销来处理引用。
4.清理子阶段
再标记阶段之后进入清理子阶段,也是需要STW的。清理子阶段主要执行以下操作:
- 统计存活对象,这是利用RSet和BitMap来完成的,统计的结果将会用来排序分区,以用于下一次的CSet的选择;根据SATB算法,需要把新分配的对象,即不在本次并发标记范围内的新分配对象,都视为活跃对象。
- 交换标记位图,为下次并发标记准备。
- 重置RSet,此时老生代分区已经标记完成,如果标记后的分区没有引用对象,这说明引用已经改变,这个时候可以删除原来的RSet里面的引用关系。
- 把空闲分区放到空闲分区列表中;这里的空闲指的是全都是垃圾对象的分区;如果分区还有任何分区活跃对象都不会释放,真正释放是在混合GC中。
该阶段比较容易引起误解地方在于,清理操作并不会清理垃圾对象,也不会执行存活对象的拷贝。也就是说,在极端情况下,该阶段结束之后,空闲分区列表将毫无变化,JVM的内存使用情况也毫无变化。
5.混合回收阶段的分析
在介绍并发标记的时候我们提到使用SATB来存储变更前的引用关系,把变更前的对象都作为活跃对象进行标记,保证了标记的正确性。但是在G1并发标记的实现步骤中,提到并发标记是以Survivor分区为根对整个老生代进行标记。这里有没有问题?实际上存在Java根直接引用老生代对象,而不存在新生代对象到老生代的引用,由此来看并发标记并不是对老生代的完全标记,老生代分区里面可能存在一些对象是通过Java根到达的。这些对象在并发标记的时候并不会被标记,所以导致可能存活的对象因没有标记而被错误回收。
从这一点来说,仅仅收集Survivor是不够的,但是只需要把直接从根出发到老生代的引用或者大对象分区的引用补上就完整了。其实这个内容前面我们已经提到,这里为了加深理解,我们把代码再拿出来,如下所示:
src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp
template <G1Barrier barrier, G1Mark do_mark_object>
template <class T>
void G1ParCopyClosure<barrier, do_mark_object>::do_oop_work(T* p) {
T heap_oop = oopDesc::load_heap_oop(p);
if (oopDesc::is_null(heap_oop)) {
return;
}
oop obj = oopDesc::decode_heap_oop_not_null(heap_oop);
assert(_worker_id == _par_scan_state->queue_num(), "sanity");
const InCSetState state = _g1->in_cset_state(obj);
if (state.is_in_cset()) {
// 前面提到过对象所在分区处于CSet中,可以复制
oop forwardee;
markOop m = obj->mark();
if (m->is_marked()) {
forwardee = (oop) m->decode_pointer();
} else {
forwardee = _par_scan_state->copy_to_survivor_space(state, obj, m);
}
assert(forwardee != NULL, "forwardee should not be NULL");
oopDesc::encode_store_heap_oop(p, forwardee);
if (do_mark_object != G1MarkNone && forwardee != obj) {
// If the object is self-forwarded we don't need to explicitly
// mark it, the evacuation failure protocol will do so.
mark_forwarded_object(obj, forwardee);
}
if (barrier == G1BarrierKlass) {
do_klass_barrier(p, forwardee);
}
} else {
// 对于不在CSet中的对象,先把对象标记为活的,在并发标记的时候认为是根对象并作并发标记
// 如果是大对象,直接把大对象标记为活跃对象
if (state.is_humongous()) {
_g1->set_humongous_is_live(obj);
}
// The object is not in collection set. If we're a root scanning
// closure during an initial mark pause then attempt to mark the object.
// 如果发现处于并发标记周期前的YGC,则需要把对象放入到标记栈
if (do_mark_object == G1MarkFromRoot) {
mark_object(obj);
/* 这里的mark_object其实就是调用_cm->grayRoot(obj, (size_t) obj->size(),_worker_id),
也就是把这个对象标记为灰色,在并发标记的时候作为根,稍后分析Survivor分区处理的时候同样调用这个代码。留待后续分析*/
}
}
if (barrier == G1BarrierEvac) {
_par_scan_state->update_rs(_from, p, _worker_id);
}
}
在这个模板类中为了区别一般的YGC和混合GC的初始标记阶段,使用了一个参数do_mark_object,当进行一般的YGC时,参数设置为G1MarkNone,当发现开启了并发标记则设置为G1MarkFromRoot。
混合回收中并发标记处理的线程
混合回收中,并发标记的代码在concurrentMarkThread::run中。根据并发标记流程图非常容易理解实现。我们看一下关键点,代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMarkThread.cpp
void ConcurrentMarkThread::run() {
initialize_in_thread();
_vtime_start = os::elapsedVTime();
wait_for_universe_init();
G1CollectedHeap* g1h = G1CollectedHeap::heap();
G1CollectorPolicy* g1_policy = g1h->g1_policy();
G1MMUTracker *mmu_tracker = g1_policy->mmu_tracker();
Thread *current_thread = Thread::current();
while (!_should_terminate) {
// 并发标记线程在创建后并不会立即启动,在一定的条件下才能启动
// wait until started is set.
sleepBeforeNextCycle();
if (_should_terminate) {
break;
}
{
ResourceMark rm;
HandleMark hm;
double cycle_start = os::elapsedVTime();
// We have to ensure that we finish scanning the root regions
// before the next GC takes place. To ensure this we have to
// make sure that we do not join the STS until the root regions
// have been scanned. If we did then it's possible that a
// subsequent GC could block us from joining the STS and proceed
// without the root regions have been scanned which would be a
// correctness issue.
double scan_start = os::elapsedTime();
// 并发标记启动之后,从Survivor分区开始进行扫描
if (!cm()->has_aborted()) {
if (G1Log::fine()) {
gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
gclog_or_tty->print_cr("[GC concurrent-root-region-scan-start]");
}
_cm->scanRootRegions();
double scan_end = os::elapsedTime();
if (G1Log::fine()) {
gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
gclog_or_tty->print_cr("[GC concurrent-root-region-scan-end, %1.7lf secs]",
scan_end - scan_start);
}
}
double mark_start_sec = os::elapsedTime();
if (G1Log::fine()) {
gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
gclog_or_tty->print_cr("[GC concurrent-mark-start]");
}
int iter = 0;
do {
iter++;
// 这是并发标记子阶段的地方
if (!cm()->has_aborted()) {
_cm->markFromRoots();
}
double mark_end_time = os::elapsedVTime();
double mark_end_sec = os::elapsedTime();
_vtime_mark_accum += (mark_end_time - cycle_start);
// 这是再标记操作
if (!cm()->has_aborted()) {
if (g1_policy->adaptive_young_list_length()) {
double now = os::elapsedTime();
double remark_prediction_ms = g1_policy->predict_remark_time_ms();
jlong sleep_time_ms = mmu_tracker->when_ms(now, remark_prediction_ms);
os::sleep(current_thread, sleep_time_ms, false);
}
if (G1Log::fine()) {
gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
gclog_or_tty->print_cr("[GC concurrent-mark-end, %1.7lf secs]",
mark_end_sec - mark_start_sec);
}
CMCheckpointRootsFinalClosure final_cl(_cm);
VM_CGC_Operation op(&final_cl, "GC remark", true /* needs_pll */);
VMThread::execute(&op);
}
if (cm()->restart_for_overflow()) {
if (G1TraceMarkStackOverflow) {
gclog_or_tty->print_cr("Restarting conc marking because of MS overflow "
"in remark (restart #%d).", iter);
}
if (G1Log::fine()) {
gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
gclog_or_tty->print_cr("[GC concurrent-mark-restart-for-overflow]");
}
}
// 这里的循环是和前面并发标记子阶段的do对应的。所以在循环中执行上面的并发标记子
// 阶段的操作,当并发标记对象时如果栈空间溢出则会继续循环
} while (cm()->restart_for_overflow());
double end_time = os::elapsedVTime();
// Update the total virtual time before doing this, since it will try
// to measure it to get the vtime for this marking. We purposely
// neglect the presumably-short "completeCleanup" phase here.
_vtime_accum = (end_time - _vtime_start);
// 这是执行清理的地方
if (!cm()->has_aborted()) {
if (g1_policy->adaptive_young_list_length()) {
double now = os::elapsedTime();
double cleanup_prediction_ms = g1_policy->predict_cleanup_time_ms();
jlong sleep_time_ms = mmu_tracker->when_ms(now, cleanup_prediction_ms);
os::sleep(current_thread, sleep_time_ms, false);
}
CMCleanUp cl_cl(_cm);
VM_CGC_Operation op(&cl_cl, "GC cleanup", false /* needs_pll */);
VMThread::execute(&op);
} else {
// We don't want to update the marking status if a GC pause
// is already underway.
// 并发标记对象被终止,设置一些标志
SuspendibleThreadSetJoiner sts;
g1h->set_marking_complete();
}
// Check if cleanup set the free_regions_coming flag. If it
// hasn't, we can just skip the next step.
if (g1h->free_regions_coming()) {
// The following will finish freeing up any regions that we
// found to be empty during cleanup. We'll do this part
// without joining the suspendible set. If an evacuation pause
// takes place, then we would carry on freeing regions in
// case they are needed by the pause. If a Full GC takes
// place, it would wait for us to process the regions
// reclaimed by cleanup.
double cleanup_start_sec = os::elapsedTime();
if (G1Log::fine()) {
gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
gclog_or_tty->print_cr("[GC concurrent-cleanup-start]");
}
// Now do the concurrent cleanup operation.
_cm->completeCleanup();
// Notify anyone who's waiting that there are no more free
// regions coming. We have to do this before we join the STS
// (in fact, we should not attempt to join the STS in the
// interval between finishing the cleanup pause and clearing
// the free_regions_coming flag) otherwise we might deadlock:
// a GC worker could be blocked waiting for the notification
// whereas this thread will be blocked for the pause to finish
// while it's trying to join the STS, which is conditional on
// the GC workers finishing.
g1h->reset_free_regions_coming();
double cleanup_end_sec = os::elapsedTime();
if (G1Log::fine()) {
gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
gclog_or_tty->print_cr("[GC concurrent-cleanup-end, %1.7lf secs]",
cleanup_end_sec - cleanup_start_sec);
}
}
guarantee(cm()->cleanup_list_is_empty(),
"at this point there should be no regions on the cleanup list");
// There is a tricky race before recording that the concurrent
// cleanup has completed and a potential Full GC starting around
// the same time. We want to make sure that the Full GC calls
// abort() on concurrent mark after
// record_concurrent_mark_cleanup_completed(), since abort() is
// the method that will reset the concurrent mark state. If we
// end up calling record_concurrent_mark_cleanup_completed()
// after abort() then we might incorrectly undo some of the work
// abort() did. Checking the has_aborted() flag after joining
// the STS allows the correct ordering of the two methods. There
// are two scenarios:
//
// a) If we reach here before the Full GC, the fact that we have
// joined the STS means that the Full GC cannot start until we
// leave the STS, so record_concurrent_mark_cleanup_completed()
// will complete before abort() is called.
//
// b) If we reach here during the Full GC, we'll be held up from
// joining the STS until the Full GC is done, which means that
// abort() will have completed and has_aborted() will return
// true to prevent us from calling
// record_concurrent_mark_cleanup_completed() (and, in fact, it's
// not needed any more as the concurrent mark state has been
// already reset).
{
SuspendibleThreadSetJoiner sts;
// 这里是通知下一次GC发生时,应该启动混合YC,即要回收老生代分区
if (!cm()->has_aborted()) {
g1_policy->record_concurrent_mark_cleanup_completed();
}
}
if (cm()->has_aborted()) {
if (G1Log::fine()) {
gclog_or_tty->gclog_stamp(cm()->concurrent_gc_id());
gclog_or_tty->print_cr("[GC concurrent-mark-abort]");
}
}
// We now want to allow clearing of the marking bitmap to be
// suspended by a collection pause.
// We may have aborted just before the remark. Do not bother clearing the
// bitmap then, as it has been done during mark abort.
// 这里是在清理工作之后交换了MarkBitmap,此时需要对nextMarkBitmap重新置位,
// 便于下一次并发标记
if (!cm()->has_aborted()) {
SuspendibleThreadSetJoiner sts;
_cm->clearNextBitmap();
} else {
assert(!G1VerifyBitmaps || _cm->nextMarkBitmapIsClear(), "Next mark bitmap must be clear");
}
}
// Update the number of full collections that have been
// completed. This will also notify the FullGCCount_lock in case a
// Java thread is waiting for a full GC to happen (e.g., it
// called System.gc() with +ExplicitGCInvokesConcurrent).
{
SuspendibleThreadSetJoiner sts;
g1h->increment_old_marking_cycles_completed(true /* concurrent */);
g1h->register_concurrent_cycle_end();
}
}
assert(_should_terminate, "just checking");
terminate();
}
扫描任务处理的代码逻辑如下:
class CMRootRegionScanTask : public AbstractGangTask {
private:
ConcurrentMark* _cm;
public:
CMRootRegionScanTask(ConcurrentMark* cm) :
AbstractGangTask("Root Region Scan"), _cm(cm) { }
void work(uint worker_id) {
assert(Thread::current()->is_ConcurrentGC_thread(),
"this should only be done by a conc GC thread");
CMRootRegions* root_regions = _cm->root_regions();
HeapRegion* hr = root_regions->claim_next();
while (hr != NULL) {
_cm->scanRootRegion(hr, worker_id);
hr = root_regions->claim_next();
}
}
};
void ConcurrentMark::scanRootRegion(HeapRegion* hr, uint worker_id) {
// Currently, only survivors can be root regions.
assert(hr->next_top_at_mark_start() == hr->bottom(), "invariant");
G1RootRegionScanClosure cl(_g1h, this, worker_id);
const uintx interval = PrefetchScanIntervalInBytes;
HeapWord* curr = hr->bottom();
const HeapWord* end = hr->top();
while (curr < end) {
Prefetch::read(curr, interval);
oop obj = oop(curr);
int size = obj->oop_iterate(&cl);
assert(size == obj->size(), "sanity");
curr += size;
}
}
并发标记子阶段代码逻辑如下:
class CMConcurrentMarkingTask: public AbstractGangTask {
private:
ConcurrentMark* _cm;
ConcurrentMarkThread* _cmt;
public:
void work(uint worker_id) {
assert(Thread::current()->is_ConcurrentGC_thread(),
"this should only be done by a conc GC thread");
ResourceMark rm;
double start_vtime = os::elapsedVTime();
SuspendibleThreadSet::join();
assert(worker_id < _cm->active_tasks(), "invariant");
CMTask* the_task = _cm->task(worker_id);
the_task->record_start_time();
if (!_cm->has_aborted()) {
do {
double start_vtime_sec = os::elapsedVTime();
double mark_step_duration_ms = G1ConcMarkStepDurationMillis;
the_task->do_marking_step(mark_step_duration_ms,
true /* do_termination */,
false /* is_serial*/);
double end_vtime_sec = os::elapsedVTime();
double elapsed_vtime_sec = end_vtime_sec - start_vtime_sec;
_cm->clear_has_overflown();
_cm->do_yield_check(worker_id);
jlong sleep_time_ms;
if (!_cm->has_aborted() && the_task->has_aborted()) {
sleep_time_ms =
(jlong) (elapsed_vtime_sec * _cm->sleep_factor() * 1000.0);
SuspendibleThreadSet::leave();
os::sleep(Thread::current(), sleep_time_ms, false);
SuspendibleThreadSet::join();
}
} while (!_cm->has_aborted() && the_task->has_aborted());
}
the_task->record_end_time();
guarantee(!the_task->has_aborted() || _cm->has_aborted(), "invariant");
SuspendibleThreadSet::leave();
double end_vtime = os::elapsedVTime();
_cm->update_accum_task_vtime(worker_id, end_vtime - start_vtime);
}
CMConcurrentMarkingTask(ConcurrentMark* cm,
ConcurrentMarkThread* cmt) :
AbstractGangTask("Concurrent Mark"), _cm(cm), _cmt(cmt) { }
~CMConcurrentMarkingTask() { }
};
清理阶段:
void ConcurrentMark::cleanup() {
// world is stopped at this checkpoint
assert(SafepointSynchronize::is_at_safepoint(),
"world should be stopped");
G1CollectedHeap* g1h = G1CollectedHeap::heap();
// If a full collection has happened, we shouldn't do this.
if (has_aborted()) {
g1h->set_marking_complete(); // So bitmap clearing isn't confused
return;
}
g1h->verify_region_sets_optional();
if (VerifyDuringGC) {
HandleMark hm; // handle scope
Universe::heap()->prepare_for_verify();
Universe::verify(VerifyOption_G1UsePrevMarking,
" VerifyDuringGC:(before)");
}
g1h->check_bitmaps("Cleanup Start");
G1CollectorPolicy* g1p = G1CollectedHeap::heap()->g1_policy();
g1p->record_concurrent_mark_cleanup_start();
double start = os::elapsedTime();
HeapRegionRemSet::reset_for_cleanup_tasks();
uint n_workers;
// Do counting once more with the world stopped for good measure.
G1ParFinalCountTask g1_par_count_task(g1h, &_region_bm, &_card_bm);
if (G1CollectedHeap::use_parallel_gc_threads()) {
assert(g1h->check_heap_region_claim_values(HeapRegion::InitialClaimValue),
"sanity check");
g1h->set_par_threads();
n_workers = g1h->n_par_threads();
assert(g1h->n_par_threads() == n_workers,
"Should not have been reset");
g1h->workers()->run_task(&g1_par_count_task);
// Done with the parallel phase so reset to 0.
g1h->set_par_threads(0);
assert(g1h->check_heap_region_claim_values(HeapRegion::FinalCountClaimValue),
"sanity check");
} else {
n_workers = 1;
g1_par_count_task.work(0);
}
if (VerifyDuringGC) {
// Verify that the counting data accumulated during marking matches
// that calculated by walking the marking bitmap.
// Bitmaps to hold expected values
BitMap expected_region_bm(_region_bm.size(), true);
BitMap expected_card_bm(_card_bm.size(), true);
G1ParVerifyFinalCountTask g1_par_verify_task(g1h,
&_region_bm,
&_card_bm,
&expected_region_bm,
&expected_card_bm);
if (G1CollectedHeap::use_parallel_gc_threads()) {
g1h->set_par_threads((int)n_workers);
g1h->workers()->run_task(&g1_par_verify_task);
// Done with the parallel phase so reset to 0.
g1h->set_par_threads(0);
assert(g1h->check_heap_region_claim_values(HeapRegion::VerifyCountClaimValue),
"sanity check");
} else {
g1_par_verify_task.work(0);
}
guarantee(g1_par_verify_task.failures() == 0, "Unexpected accounting failures");
}
size_t start_used_bytes = g1h->used();
g1h->set_marking_complete();
double count_end = os::elapsedTime();
double this_final_counting_time = (count_end - start);
_total_counting_time += this_final_counting_time;
if (G1PrintRegionLivenessInfo) {
G1PrintRegionLivenessInfoClosure cl(gclog_or_tty, "Post-Marking");
_g1h->heap_region_iterate(&cl);
}
// Install newly created mark bitMap as "prev".
swapMarkBitMaps();
g1h->reset_gc_time_stamp();
// Note end of marking in all heap regions.
G1ParNoteEndTask g1_par_note_end_task(g1h, &_cleanup_list);
if (G1CollectedHeap::use_parallel_gc_threads()) {
g1h->set_par_threads((int)n_workers);
g1h->workers()->run_task(&g1_par_note_end_task);
g1h->set_par_threads(0);
assert(g1h->check_heap_region_claim_values(HeapRegion::NoteEndClaimValue),
"sanity check");
} else {
g1_par_note_end_task.work(0);
}
g1h->check_gc_time_stamps();
if (!cleanup_list_is_empty()) {
// The cleanup list is not empty, so we'll have to process it
// concurrently. Notify anyone else that might be wanting free
// regions that there will be more free regions coming soon.
g1h->set_free_regions_coming();
}
// call below, since it affects the metric by which we sort the heap
// regions.
if (G1ScrubRemSets) {
double rs_scrub_start = os::elapsedTime();
G1ParScrubRemSetTask g1_par_scrub_rs_task(g1h, &_region_bm, &_card_bm);
if (G1CollectedHeap::use_parallel_gc_threads()) {
g1h->set_par_threads((int)n_workers);
g1h->workers()->run_task(&g1_par_scrub_rs_task);
g1h->set_par_threads(0);
assert(g1h->check_heap_region_claim_values(
HeapRegion::ScrubRemSetClaimValue),
"sanity check");
} else {
g1_par_scrub_rs_task.work(0);
}
double rs_scrub_end = os::elapsedTime();
double this_rs_scrub_time = (rs_scrub_end - rs_scrub_start);
_total_rs_scrub_time += this_rs_scrub_time;
}
// this will also free any regions totally full of garbage objects,
// and sort the regions.
g1h->g1_policy()->record_concurrent_mark_cleanup_end((int)n_workers);
// Statistics.
double end = os::elapsedTime();
_cleanup_times.add((end - start) * 1000.0);
if (G1Log::fine()) {
g1h->print_size_transition(gclog_or_tty,
start_used_bytes,
g1h->used(),
g1h->capacity());
}
// Clean up will have freed any regions completely full of garbage.
// Update the soft reference policy with the new heap occupancy.
Universe::update_heap_info_at_gc();
if (VerifyDuringGC) {
HandleMark hm; // handle scope
Universe::heap()->prepare_for_verify();
Universe::verify(VerifyOption_G1UsePrevMarking,
" VerifyDuringGC:(after)");
}
g1h->check_bitmaps("Cleanup End");
g1h->verify_region_sets_optional();
// We need to make this be a "collection" so any collection pause that
// races with it goes around and waits for completeCleanup to finish.
g1h->increment_total_collections();
// Clean out dead classes and update Metaspace sizes.
if (ClassUnloadingWithConcurrentMark) {
ClassLoaderDataGraph::purge();
}
MetaspaceGC::compute_new_size();
// We reclaimed old regions so we should calculate the sizes to make
// sure we update the old gen/space data.
g1h->g1mm()->update_sizes();
g1h->allocation_context_stats().update_after_mark();
g1h->trace_heap_after_concurrent_cycle();
}
并发标记线程启动的时机
发标记线程在创建后并不会立即启动并发标记任务(并发标记任务也是通过一个线程池来运行的),而是需要一定的时机,它会等待条件成熟才启动。启动通过信的形式来通知,代码如下所示:
hotspot/src/share/vm/gc_implementation/g1/concurrentMarkThread.cpp
void ConcurrentMarkThread::sleepBeforeNextCycle() {
MutexLockerEx x(CGC_lock, Mutex::_no_safepoint_check_flag);
while (!started() && !_should_terminate) {
CGC_lock->wait(Mutex::_no_safepoint_check_flag);
}
if (started()) {
set_in_progress();// 设置状态为处理中。
clear_started();
}
}
concurrentMarkThread启动依赖于YGC。在YGC的最后阶段判定如果可以启动并发标记,则调用doConcurrentMark发送通知,代码如下所示:
src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp
void G1CollectedHeap::doConcurrentMark() {
MutexLockerEx x(CGC_lock, Mutex::_no_safepoint_check_flag);
if (!_cmThread->in_progress()) {
_cmThread->set_started();
CGC_lock->notify();
}
}
而启动这个通知是在YGC开始的时候判断,判断的依据主要是根据内存使用的情况。当老生代使用的内存加上本次即将分配的内存占到总内存的45%,就表明可以启动并发标记任务,代码如下所示:
src/share/vm/gc_implementation/g1/g1CollectorPolicy.cpp
bool G1CollectorPolicy::need_to_start_conc_mark(const char* source, size_t alloc_word_size) {
if (_g1->concurrent_mark()->cmThread()->during_cycle()) return false;
size_t marking_initiating_used_threshold = (_g1->capacity() / 100) *
InitiatingHeapOccupancyPercent;
size_t cur_used_bytes = _g1->non_young_capacity_bytes();
size_t alloc_byte_size = alloc_word_size * HeapWordSize;
if ((cur_used_bytes + alloc_byte_size) > marking_initiating_used_threshold)
return true;
return false;
}
根扫描子阶段
并发标记线程启动之后,需要开始执行扫描处理,该子阶段是并发标记的第一步。代码如下:
src\share\vm\gc_implementation\g1\concurrentMark.cpp
void ConcurrentMark::scanRootRegions() {
// Start of concurrent marking.
ClassLoaderDataGraph::clear_claimed_marks();
// scan_in_progress() will have been set to true only if there was
// at least one root region to scan. So, if it's false, we
// should not attempt to do any further work.
if (root_regions()->scan_in_progress()) {
_parallel_marking_threads = calc_parallel_marking_threads();
assert(parallel_marking_threads() <= max_parallel_marking_threads(),
"Maximum number of marking threads exceeded");
uint active_workers = MAX2(1U, parallel_marking_threads());
// 根据参数确定并行任务的数量,使用并行任务来对根(即Survivor)分区扫描
CMRootRegionScanTask task(this);
if (use_parallel_marking_threads()) {
_parallel_workers->set_active_workers((int) active_workers);
_parallel_workers->run_task(&task);
} else {
task.work(0);
}
// It's possible that has_aborted() is true here without actually
// aborting the survivor scan earlier. This is OK as it's
// mainly used for sanity checking.
// 通知锁,可以进行下一次YGC
root_regions()->scan_finished();
}
}
这里需要注意,因为混合GC依赖于YGC的Survivor区,可能发生这样一种情况,当混合GC扫描还没有结束,如果又发生了YGC,那么Survivor就会变化,这对混合GC来说是不可接受的,因为它不能准确地标记对象。所以在混合GC的时候一定会要求做完Survivor分区的扫描之后才能再进行一次新的YGC。
这个实现机制是通过锁和通知完成的。如在do_collection或者do_collection_pause_at_safepoint真正进行垃圾回收之前,会先调用wait_until_scan_finished判断是否能够启动垃圾回收。这也是通过信号完成的。代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
bool CMRootRegions::wait_until_scan_finished() {
if (!scan_in_progress()) return false;
{
MutexLockerEx x(RootRegionScan_lock, Mutex::_no_safepoint_check_flag);
while (scan_in_progress()) {
RootRegionScan_lock->wait(Mutex::_no_safepoint_check_flag);
}
}
return true;
}
以上代码就是判定锁是否得到通知。锁的释放在根扫描的最后一步root_regions()->scan_finished()中,对锁发送通知RootRegionScan_lock->notify_all()。
并行扫描任务线程的数目通过参数ConcGCThreads来设置,默认值为0,这个数值可以启发式推断。
- ·ConcGCThreads并发线程数,默认值为0,如果没有设置则动态调整。
- ·如果设置了参数G1MarkingOverheadPercent,默认值为0,则ConcGCThreads = ncpus × G1MarkingOverheadPercent × MaxGCPauseMillis / GCPauseIntervalMillis。这表示ConcGCThreads会根据GC负载占比来推断。
- ·如果没有设置,则使用ParallelGCThreads(前文介绍过推断依据)为依据来推断。ConcGCThreads = (ParallelGCThreads+2)/4,最小值为1。
- ·判断线程数是否可以动态调整。
- ·如果设置了参数G1MarkingOverheadPercent,默认值为0,则ConcGCThreads依赖于参数UseDynamicNumberOfGCThreads(默认值为false)和ForceDynamicNumberOfGCThreads(默认值为false)。当关闭UseDynamicNumberOfGCThreads,或者设置了ConcGCThreads并且关闭ForceDynamicNumberOfGCThreads,表示不允许动态调整,则使用ConcGCThreads的值为并行线程任务数。
- ·如果可以动态调整线程数目,将根据Mutator线程数目×2和堆空间的大小/HeapSizePerGCThread(默认值为64M)的最大值作为新的并发线程数。并且最大值不能超过我们在第一步算出来的ConcGCThreads个数。如果算出来的并发数比当前的值大,直接使用;如果算出来的值比当前使用的并发数小,则取这两个数的中值。
- ·如果打开动态调整,可以打开开关TraceDynamicGCThreads输出线程并发数变化信息。
GC并发线程动态化调整是JDK8才引入的,这个参数的最大值会受限于第一步中ConcGCThreads,它最大的用处在于当GC负载比较低,可以减少GC线程,让应用线程更多地抢占到CPU。目前常见的参数调整是,如果发现GC并发线程花费的时间比较多,可以调整ParallelGCThreads增加线程数量,也可以直接调整ConcGCThreads增加(如果调整这个值,它的最大值不能超过ParallelGCThreads,否则无效)。
其中CMRootRegionScanTask作为并发标记任务,它会先扫描活跃的根分区,然后对每一个分区进行处理。我们直接看work方法,代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
class CMRootRegionScanTasK::work(uint worker_id) {
// 获得要扫描的根分区
CMRootRegions* root_regions = _cm->root_regions();
HeapRegion* hr = root_regions->claim_next();
while (hr != NULL) {
// 针对每一个分区处理
_cm->scanRootRegion(hr, worker_id);
hr = root_regions->claim_next();
}
}
};
根分区是如何获得的?在YGC结束阶段,会把Survivor区作为CM扫描时的根,通过concurrent_mark() -> checkpointRootsInitialPost()触发,然后设置准备扫描的根,代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
void CMRootRegions::prepare_for_scan() {
// 在CM的时候,只要扫描Survivor即可
_next_survivor = _young_list->first_survivor_region();
_scan_in_progress = (_next_survivor != NULL);
_should_abort = false;
}
分区扫描处理,主要是通过G1RootRegionScanClosure完成。注意,在分区处理的时候需要对整个分区完全处理,所以需要遍历整个有效分区(从bottom到top指针之间)。代码如下:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
void ConcurrentMark::scanRootRegion(HeapRegion* hr, uint worker_id) {
G1RootRegionScanClosure cl(_g1h, this, worker_id);
HeapWord* curr = hr->bottom();
const HeapWord* end = hr->top();
while (curr < end) {
oop obj = oop(curr);
int size = obj->oop_iterate(&cl);
curr += size;
}
}
这个辅助类最终会调用到ConcurrentMark::grayRoot,这里完成最主要的工作就是对对象完成并发标记和计数。代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.inline.hpp
inline void ConcurrentMark::grayRoot(oop obj, size_t word_size,
uint worker_id, HeapRegion* hr) {
……
if (addr < hr->next_top_at_mark_start()) {
if (!_nextMarkBitMap->isMarked(addr)) {
par_mark_and_count(obj, word_size, hr, worker_id);
}
}
}
// 其中的标记计数动作也在这个文件中
inline bool ConcurrentMark::par_mark_and_count(oop obj,
size_t word_size,
HeapRegion* hr,
uint worker_id) {
HeapWord* addr = (HeapWord*)obj;
// 并发的标记这个地址指向的对象是存活的
if (_nextMarkBitMap->parMark(addr)) {
MemRegion mr(addr, word_size);
// 记录这个对象所在的卡表是有效,即标记为1
count_region(mr, hr, worker_id);
return true;
}
return false;
}
为什么标记计数是并发?因为是多个并发标记线程,但是这多个线程共享同一个空间_nextMarkBitMap,所以这时候需要并发标记对象,并发标记对象实质上就是用CAS完成串行的位操作。计数有什么用处?因为是多个并发标记线程,但是这多个线程数据并不共享,访问每个线程的位图,所以不会竞争。count_region最主要的目的是为了计算活跃内存的大小。
并发标记子阶段
根扫描结束之后,就进入了并发标记子阶段,具体在ConcurrentMark::markFromRoots()中,它和我们前面提到的scanFromRoots()非常类似。
void ConcurrentMark::markFromRoots() {
// we might be tempted to assert that:
// assert(asynch == !SafepointSynchronize::is_at_safepoint(),
// "inconsistent argument?");
// However that wouldn't be right, because it's possible that
// a safepoint is indeed in progress as a younger generation
// stop-the-world GC happens even as we mark in this generation.
_restart_for_overflow = false;
force_overflow_conc()->init();
// _g1h has _n_par_threads
_parallel_marking_threads = calc_parallel_marking_threads();
assert(parallel_marking_threads() <= max_parallel_marking_threads(),
"Maximum number of marking threads exceeded");
uint active_workers = MAX2(1U, parallel_marking_threads());
// Parallel task terminator is set in "set_concurrency_and_phase()"
set_concurrency_and_phase(active_workers, true /* concurrent */);
CMConcurrentMarkingTask markingTask(this, cmThread());
if (use_parallel_marking_threads()) {
_parallel_workers->set_active_workers((int)active_workers);
// Don't set _n_par_threads because it affects MT in process_roots()
// and the decisions on that MT processing is made elsewhere.
assert(_parallel_workers->active_workers() > 0, "Should have been set");
_parallel_workers->run_task(&markingTask);
} else {
markingTask.work(0);
}
print_stats();
}
我们只看标记的具体工作CMConcurrentMarkingTask::work,代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
void CMConcurrentMarkingTask::work(uint worker_id) {
// 当发生同步时,进行等待,否则继续
SuspendibleThreadSet::join();
CMTask* the_task = _cm->task(worker_id);
if (!_cm->has_aborted()) {
do {
// 设置标记目标时间,G1ConcMarkStepDurationMillis默认值是10ms,
// 表示并发标记子阶段在10ms内完成。
double mark_step_duration_ms = G1ConcMarkStepDurationMillis;
the_task->do_marking_step(mark_step_duration_ms,
true /* do_termination */,
false /* is_serial*/);
...
_cm->clear_has_overflown();
_cm->do_yield_check(worker_id);
……
// CM任务结束后还可以睡眠一会
} while (!_cm->has_aborted() && the_task->has_aborted());
}
SuspendibleThreadSet::leave();
_cm->update_accum_task_vtime(worker_id, end_vtime - start_vtime);
}
具体的处理在do_marking_step中,主要包含两步:
- 处理SATB缓存。
- 根据已经标记的分区nextMarkBitmap的对象进行处理,处理的方式是针对已标记对象的每一个field进行递归并发标记。
代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
void CMTask::do_marking_step(double time_target_ms,
bool do_termination,
bool is_serial) {
……
// 根据过去运行的标记信息,预测本次标记要花费的时间
double diff_prediction_ms = g1_policy->get_new_prediction(&_marking_step_diffs_ms);
_time_target_ms = time_target_ms - diff_prediction_ms;
// 这里设置的closure会在后面用到
CMBitMapClosure bitmap_closure(this, _cm, _nextMarkBitMap);
G1CMOopClosure cm_oop_closure(_g1h, _cm, this);
set_cm_oop_closure(&cm_oop_closure);
// 处理SATB队列
drain_satb_buffers();
// 根据根对象标记时发现的对象开始处理。在上面已经介绍过。
drain_local_queue(true);
// 针对全局标记栈开始处理,注意这里为了效率,只有当全局标记栈超过1/3才会开始处理。
// 处理的思路很简单,就是把全局标记栈的对象移入CMTask的队列中,等待处理。
drain_global_stack(true);
do {
if (!has_aborted() && _curr_region != NULL) {
……
// 这个MemRegion是新增的对象,所以从finger开始到结束全部开始标记
MemRegion mr = MemRegion(_finger, _region_limit);
if (mr.is_empty()) {
giveup_current_region();
regular_clock_call();
} else if (_curr_region->isHumongous() && mr.start() == _curr_region->bottom()) {
/*如果是大对象,并且该分区是该对象的最后一个分区,则:
1)如果对象被标记,说明这个对象需要被作为灰对象处理。处理在CMBitMapClosure::do_bit 中。
2)对象没有标记,直接结束本分区。*/
if (_nextMarkBitMap->isMarked(mr.start())) {
BitMap::idx_t offset = _nextMarkBitMap->heapWordToOffset(mr.start());
bitmap_closure.do_bit(offset);
/*do_bit所做的事情有:
1)调整finger,处理本对象(准确地说是处理对象的Field所指向的oop对象),
处理是调用process_grey_object<true>(obj),所以实际上形成递归
2)然后处理本地队列。
3)处理全局标记栈。*/
}
giveup_current_region();
regular_clock_call();
} else if (_nextMarkBitMap->iterate(&bitmap_closure, mr)) {
// 处理本分区的标记对象,这里会对整个分区里面的对象调用CMBitMapClosure::do_bit
完成标记,实际上形成递归。
giveup_current_region();
regular_clock_call();
} else {
……
}
}
// 再次处理本地队列和全局标记栈。实际上这是为了后面的加速,标记发生时,会有新的对象进来。
drain_local_queue(true);
drain_global_stack(true);
while (!has_aborted() && _curr_region == NULL && !_cm->out_of_regions()) {
/*标记本分区已经被处理,这时可以修改全局的finger。注意,在这里是每个线程都将获得分区,获取的逻辑在claim_region:所有的CM线程都去竞争全局finger指向的分区(使用CAS),并设置全局finger到下一个分区的起始位置。当所有的分区都遍历完了之后,即全局finger到达整个堆空间的最后,这时claimed_region就会为NULL,也就是说NULL表示所有的分区都处理完了。*/
HeapRegion* claimed_region = _cm->claim_region(_worker_id);
……
setup_for_region(claimed_region);
……
}
// 这个循环会继续,只要分区不为NULL,并且没有被终止,这就是前面调用giveup_current_region
// 和regular_clock_call的原因,就是为了中止循环。
} while ( _curr_region != NULL && !has_aborted());
if (!has_aborted()) {
// 再处理一次SATB缓存,那么再标记的时候工作量就少了
drain_satb_buffers();
}
// 这个时候需要把本地队列和全局标记栈全部处理掉。
drain_local_queue(false);
drain_global_stack(false);
// 尝试从其他的任务的队列中偷窃任务,这是为了更好的性能
if (do_stealing && !has_aborted()) {
while (!has_aborted()) {
oop obj;
if (_cm->try_stealing(_worker_id, &_hash_seed, obj)) {
scan_object(obj);
drain_local_queue(false);
drain_global_stack(false);
} else {
break;
}
}
}
……
}
其中SATB的处理在drain_satb_buffers中,代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
void CMTask::drain_satb_buffers() {
……
CMSATBBufferClosure satb_cl(this, _g1h);
SATBMarkQueueSet& satb_mq_set = JavaThread::satb_mark_queue_set();
// 因为CM线程和Mutator并发运行,所以Mutator的SATB不断地变化,这里只对放入queue
// set中的SATB队列处理。
while (!has_aborted() && satb_mq_set.apply_closure_to_completed_buffer
(&satb_cl)) {
……
regular_clock_call();
}
// 因为标记需要对老生代进行,可能要花费的时间比较多,所以增加了标记检查,如果发现有溢出,
// 终止。线程同步等满足终止条件的情况都会设置停止标志来终止标记动作。
decrease_limits();
}
SATB队列的结构和前面提到的dirty card队列非常类似,处理方式也非常类似。所以只需要关注不同点:
- SATB队列的长度为1k,是由参数G1SATBBufferSize控制,表示每个队列有1000个对象。
- SATB缓存针对每个队列有一个参数G1SATBBufferEnqueueingThresholdPercent(默认值是60),表示当一个队列满了之后,首先进行过滤处理,过滤后如果使用率超过这个阈值则新分配一个队列,否则重用这个队列。过滤的条件就是这个对象属于新分配对象(位于NTAMS之下),且还没有标记,后续会处理该对象。
这里的处理逻辑主要是在CMSATBBufferClosure::do_entry中。调用路径从SATBMarkQueueSet::apply_closure_to_completed_buffer到CMSATBBufferClosure :: do_buffer再到CMSATBBufferClosure::do_entry。源码中似乎是有一个bug,在SATBMarkQueueSet :: apply_closure_to_completed_buffer和CMSATBBufferClosure::do_buffer里面都是用循环处理,这将导致一个队列被处理两次。实际上这是为了修复一个bug[插图]引入的特殊处理(这个bug和humongous对象的处理有关)。
这里需要提示一点,因为SATB set是一个全局的变量,所以使用的时候会使用锁,每个CMTask用锁摘除第一元素后就可以释放锁了。
CMSATBBufferClosure::do_entry的代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
void CMSATBBufferClosure::do_entry(void* entry) const {
……
HeapRegion* hr = _g1h->heap_region_containing_raw(entry);
if (entry < hr->next_top_at_mark_start()) {
oop obj = static_cast<oop>(entry);
_task->make_reference_grey(obj, hr);
}
}
主要工作在make_reference_grey中,代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
void CMTask::make_reference_grey(oop obj, HeapRegion* hr) {
// 对对象进行标记和计数
if (_cm->par_mark_and_count(obj, hr, _marked_bytes_array, _card_bm)) {
HeapWord* global_finger = _cm->finger();
if (is_below_finger(obj, global_finger)) {
// 对象是一个原始(基本类型)的数组,无须继续追踪。直接记录对象的长度
if (obj->is_typeArray()) {
// 这里传入的模版参数为false,说明对象是基本对象,意味着没有field要处理。
process_grey_object<false>(obj);
} else {
// ①
push(obj);
}
}
}
}
在上面代码①处,把对象入栈,这个对象可能是objArray、array、instance、instacneRef、instacneMirror等,等待后续处理。在后续的处理中,实际上通过G1CMOopClosure::do_oop最终会调用的是process_grey_object<true>(obj),该方法对obj标记同时处理每一个字段。
这个处理实际上就是对对象遍历,然后对每一个field标记处理。和前面在copy_to_surivivor_space中稍有不同,那个方法是对象从最后一个字段复制,这里是从第一个字段开始标记。为什么?这里需要考虑push对应的队列大小。在这里队列的大小固定默认值是16k(32位JVM)或者128k(64位JVM)。几个队列会组成一个queue set,这个集合的大小和ParallelGCThreads一致。当push对象到队列中时,可能会发生溢出(即超过CMTask中队列的最大值),这时候需要把CMTask中的待处理对象(这里就是灰色对象)放入到全局标记栈(globalmark stack)中。这个全局标记栈的大小可以通过参数设置。
这里有两个参数分别为:MarkStackSize和MarkStackSizeMax,在32位JVM中设置为32k和4M,64位中设置为4M和512M。如果没有设置G1可以启发式推断,确保MarkStackSize最小为32k(或者和并发线程参数ParallelGCThreads正相关,如ParallelGCThreads=8,则32位JVM中MarkStackSize=8×16k=128K,其中16k是队列的大小)。
全局标记栈仍然可能发生溢出,当溢出发生时会做两个事情:
- ·设置标记终止,并在合适的时机终止本任务(CMTask)的标记动作。
- ·尝试去扩展全局标记栈。
最后再谈论一下finger,实际上有两个finger:一个是全局的,一个是每个CMTask中的finger。全局的finger在CM初始化时是分区的起始地址。随着对分区的处理,这个finger会随之调整。简单地说在finger之下的地址都认为是新加入的对象,认为是活跃对象。局部的finger指的是每个CMTask的nextMarkBitMap指向的起始位置,在这个位置之下也说明该对象是新加入的,还是活跃对象。引入局部finger可以并发处理,加快速度。
再标记子阶段
我们继续往下走,当并发标记子阶段结束就会进入到再标记子阶段,主要是对SATB处理,它需要STW,最终会调用ConcurrentMark ::checkpointRootsFinal,代码如下所示:
hotspot/src/share/vm/gc_implementation/g1/concurrentMark.cpp
void ConcurrentMark::checkpointRootsFinal(bool clear_all_soft_refs) {
// 告诉GC这是一个非FGC,其他类型的GC
SvcGCMarker sgcm(SvcGCMarker::OTHER);
g1h->check_bitmaps("Remark Start");
checkpointRootsFinalWork();
// 处理引用
weakRefsWork(clear_all_soft_refs);
……
}
checkpointRootsFinalWork是再标记主要工作的地方。其处理思路和前面提到的扫描和标记类似,但处理方法不同,这里需要STW,所以再标记是并行处理的。任务处理在CMRemarkTask中,代码如下:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
CMRemarkTask::work(uint worker_id) {
if (worker_id < _cm->active_tasks()) {
CMTask* task = _cm->task(worker_id);
task->record_start_time();
{
// 再次处理所有线程的SATB
G1RemarkThreadsClosure threads_f(G1CollectedHeap::heap(), task, !_is_serial);
Threads::threads_do(&threads_f);
// 处理工作通过Closure里面的do_thread完成
G1RemarkThreadsClosure::do_thread(Thread* thread) {
if (thread->is_Java_thread()) {
if (thread->claim_oops_do(_is_par, _thread_parity)) {
JavaThread* jt = (JavaThread*)thread;
/*先对nmethods处理,主要是为了标识正在运行的方法活跃的栈对象,以及弱引用的对象。理论上并不需要进行这一步处理,但实际上JVM很复杂,在一些特殊的情况下通过类加载器访问到的对象都应该出现在SATB,但是SATB可能存储的对象并不一致,所以遍历nmenthod再次处理mutator的SATB。*/
jt->nmethods_do(&_code_cl);
jt->satb_mark_queue().apply_closure_and_empty(&_cm_satb_cl);
}
} else if (thread->is_VM_thread()) {
if (thread->claim_oops_do(_is_par, _thread_parity)) {
// 对于非Mutator,SATB的变化对象都在共享SATB中。
JavaThread::satb_mark_queue_set().shared_satb_queue()->
apply_closure_and_empty(&_cm_satb_cl);
}
}
}
}
// 再次进行标记,这时候的标记时间非常长,1000000秒(超过11天),这表示无论如何再标记
// 都要完成,有关内容前面已经介绍过,不再赘述
do {
task->do_marking_step(1000000000.0 /* something very large */,
true /* do_termination */,
_is_serial);
} while (task->has_aborted() && !_cm->has_overflown());
}
}
清理子阶段
下面就进入了清理子阶段,这个阶段也需要STW。清理阶段的任务我们前面已经提到,包括:分区信息计数、额外处理、RSet清理等。清理阶段通过VMThread之后最终会调用ConcurrentMark::cleanup中,代码如下所示:
hotspot/src/share/vm/gc_implementation/g1/concurrentMark.cpp
ConcurrentMark::cleanup() {
// 对分区进行计数,这样可以确定活着的对象,通过G1ParFinalCountTask并行执行
G1ParFinalCountTask g1_par_count_task(g1h, &_region_bm, &_card_bm);
if (G1CollectedHeap::use_parallel_gc_threads()) {
g1h->set_par_threads();
n_workers = g1h->n_par_threads();
g1h->workers()->run_task(&g1_par_count_task);
g1h->set_par_threads(0);
} else {
n_workers = 1;
g1_par_count_task.work(0);
}
// 如果VerifyDuringGC打开(默认值为false),则输出验证信息,这部分代码省略
……
// 如果G1PrintRegionLivenessInfo打开(默认值为false),在清理阶段输出分区信息
if (G1PrintRegionLivenessInfo) {
G1PrintRegionLivenessInfoClosure cl(gclog_or_tty, "Post-Marking");
_g1h->heap_region_iterate(&cl);
}
// 把当前的BitMap和prevbitmap互换,说明这一次所有的内存已经清理结束了。
swapMarkBitMaps();
// 对整个堆分区增加一些额外信息,通过并行任务G1ParNoteEndTask完成
G1ParNoteEndTask g1_par_note_end_task(g1h, &_cleanup_list);
if (G1CollectedHeap::use_parallel_gc_threads()) {
g1h->set_par_threads((int)n_workers);
g1h->workers()->run_task(&g1_par_note_end_task);
g1h->set_par_threads(0);
} else {
g1_par_note_end_task.work(0);
}
// 当G1ScrubRemSets打开(默认值为true,这是一个开发选项,发布版本不能更改),
// 通过G1ParScrubRemSetTask并行清理Rset,这会影响CSet的选择
if (G1ScrubRemSets) {
double rs_scrub_start = os::elapsedTime();
G1ParScrubRemSetTask g1_par_scrub_rs_task(g1h, &_region_bm, &_card_bm);
if (G1CollectedHeap::use_parallel_gc_threads()) {
g1h->set_par_threads((int)n_workers);
g1h->workers()->run_task(&g1_par_scrub_rs_task);
g1h->set_par_threads(0);
} else {
g1_par_scrub_rs_task.work(0);
}
}
// 对老生代回收集合进行处理,主要是添加CSet Chooser并对分区排序
g1h->g1_policy()->record_concurrent_mark_cleanup_end((int)n_workers);
// ClassUnloadingWithConcurrentMark默认值为true,卸载已经加载的类
if (ClassUnloadingWithConcurrentMark) {
// 这里稍微提示一下,当并发标记子阶段结束后,已经知道了哪些类加载里面的加载Java类是
// 活跃的,所以可以在此处清除。清除的入口是通过ClassLoader
ClassLoaderDataGraph::purge();
}
MetaspaceGC::compute_new_size();
// 因为可能回收了空的老生代分区,所以需要更新大小信息
g1h->g1mm()->update_sizes();
……
}
1.对分区进行计数
这部分主要是并行工作,每个不同的线程处理不同的分区,最后汇总到卡表中。代码如下所示:
hotspot/src/share/vm/gc_implementation/g1/concurrentMark.cpp
G1ParFinalCountTask::work(uint worker_id) {
FinalCountDataUpdateClosure final_update_cl(
_g1h, _actual_region_bm, _actual_card_bm);
if (G1CollectedHeap::use_parallel_gc_threads()) {
_g1h->heap_region_par_iterate_chunked(&final_update_cl,
worker_id, _n_workers, HeapRegion::FinalCountClaimValue);
} else {
_g1h->heap_region_iterate(&final_update_cl);
}
}
主要工作在FinalCountDataUpdateClosure中,处理已经标记的对象,还有所有新分配的对象都认为是活跃的。代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
FinalCountDataUpdateClosure::doHeapRegion(HeapRegion* hr) {
if (hr->continuesHumongous()) return false;
HeapWord* ntams = hr->next_top_at_mark_start();
HeapWord* top = hr->top();
// 如果在开始标记之后又有新的对象分配,需要额外处理
if (ntams < top) {
// 标记该分区有活跃对象
set_bit_for_region(hr);
// 把[ntams, top)范围内新的对象都标记到卡表
BitMap::idx_t start_idx = _cm->card_bitmap_index_for(ntams);
BitMap::idx_t end_idx = _cm->card_bitmap_index_for(top);
if (_g1h->is_in_g1_reserved(top) && !_ct_bs->is_card_aligned(top)) end_
idx += 1;
_cm->set_card_bitmap_range(_card_bm, start_idx, end_idx, true /* is_
par */);
}
// 再次标记该分区有活跃对象
if (hr->next_marked_bytes() > 0) set_bit_for_region(hr);
return false;
}
2.额外信息处理
对整个堆分区中完全空白的老生代和大对象分区进行释放,对于其他的分区处理RSet,主要是分区的RSet粒度如果发生了变化,那么变化前的数据结构可以被清除。我们来看一下具体的代码,如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
G1ParNoteEndTask::work(uint worker_id) {
HRRSCleanupTask hrrs_cleanup_task;
G1NoteEndOfConcMarkClosure g1_note_end(_g1h, &local_cleanup_list,
&hrrs_cleanup_task);
// 对所有分区处理,处理工作在G1NoteEndOfConcMarkClosure::doHeapRegion中
if (G1CollectedHeap::use_parallel_gc_threads()) {
_g1h->heap_region_par_iterate_chunked(&g1_note_end, worker_id,
_g1h->workers()->active_workers(),
HeapRegion::NoteEndClaimValue);
} else {
_g1h->heap_region_iterate(&g1_note_end);
}
// 有大对象分区和老生代分区
_g1h->remove_from_old_sets(g1_note_end.old_regions_removed(), g1_note_end.
humongous_regions_removed());
{
// 打印GC信息,同时添加释放列表,清除SPRT信息
G1HRPrinter* hr_printer = _g1h->hr_printer();
if (hr_printer->is_active()) {
FreeRegionListIterator iter(&local_cleanup_list);
while (iter.more_available()) {
HeapRegion* hr = iter.get_next();
hr_printer->cleanup(hr);
}
}
// 清除RSet可能过时的存储结构
_cleanup_list->add_ordered(&local_cleanup_list);
HeapRegionRemSet::finish_cleanup_task(&hrrs_cleanup_task);
}
}
识别老生代/大对象分区、释放识别和处理RSet结构,具体的处理逻辑在G1NoteEndOfConcMarkClosure中,代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
G1NoteEndOfConcMarkClosure::doHeapRegion(HeapRegion *hr) {
if (hr->continuesHumongous()) return false;
if (hr->used() > 0 && hr->max_live_bytes() == 0 && !hr->is_young()) {
_freed_bytes += hr->used();
// 把垃圾老生代分区加入到待释放队列
if (hr->isHumongous()) {
_humongous_regions_removed.increment(1u, hr->capacity());
_g1->free_humongous_region(hr, _local_cleanup_list, true);
} else {
_old_regions_removed.increment(1u, hr->capacity());
_g1->free_region(hr, _local_cleanup_list, true);
}
} else {
// 把对象的HRRSCleanupTask加入到RSet清除任务中
hr->rem_set()->do_cleanup_work(_hrrs_cleanup_task);
}
……
}
3.RSet清理
当G1ScrubRemSets打开时(默认值为true,这是一个开发选项,发布版本不能更改),通过G1ParScrubRemSetTask并行清理Rset,这会影响CSet的选择,代码如下所示:
src/share/vm/gc_implementation/g1/concurrentMark.cpp
G1ParScrubRemSetTask::work(uint worker_id) {
if (G1CollectedHeap::use_parallel_gc_threads()) {
_g1rs->scrub_par(_region_bm, _card_bm, worker_id,
HeapRegion::ScrubRemSetClaimValue);
} else {
_g1rs->scrub(_region_bm, _card_bm);
}
}
这样,最终会调用OtherRegionsTable::scrub完成对RSet的清理。这里有一个开关G1RSScrubVerbose(默认值为false)选项,打开时可以查看清理的信息。
4.老生代回收集处理
这就是判断哪些分区可以放入到老生代回收集合中,主要是根据老生代分区的垃圾空闲的情况,只有达到收集的阈值才可能被加入到CSet Chooser,另外会对分区进行排序,排序的依据是gc_efficiency,我们来看它的实现,代码如下:
hotspot/src/share/vm/gc_implementation/g1/g1CollectorPolicy.cpp
G1CollectorPolicy::record_concurrent_mark_cleanup_end(int no_of_gc_threads) {
_collectionSetChooser->clear();
uint region_num = _g1->num_regions();
if (G1CollectedHeap::use_parallel_gc_threads()) {
const uint OverpartitionFactor = 4;
uint WorkUnit;
// 设置并行工作的线程数目,通过ParKnownGarbageTask来完成,确定可回收的分区
if (no_of_gc_threads > 0) {
const uint MinWorkUnit = MAX2(region_num / no_of_gc_threads, 1U);
WorkUnit = MAX2(region_num / (no_of_gc_threads * OverpartitionFactor),
MinWorkUnit);
} else {
const uint MinWorkUnit = MAX2(region_num / (uint) ParallelGCThreads,
1U);
WorkUnit = MAX2(region_num / (uint) (ParallelGCThreads * OverpartitionFactor),
MinWorkUnit);
}
_collectionSetChooser->prepare_for_par_region_addition(_g1->num_regions(),
WorkUnit);
ParKnownGarbageTask parKnownGarbageTask(_collectionSetChooser,(int)
WorkUnit);
_g1->workers()->run_task(&parKnownGarbageTask);
} else {
KnownGarbageClosure knownGarbagecl(_collectionSetChooser);
_g1->heap_region_iterate(&knownGarbagecl);
}
_collectionSetChooser->sort_regions();
……
}
ParKnownGarbageTask最主要的任务就是把分区放入到CSet Chooser中。其主要工作在ParKnownGarbageHRClosure::doHeapRegion中,代码如下所示:
src/share/vm/gc_implementation/g1/g1CollectorPolicy.cpp
ParKnownGarbageHRClosure::doHeapRegion(HeapRegion* r) {
// ①
if (r->is_marked()) {
// ②
if (_cset_updater.should_add(r) && !_g1h->is_old_gc_alloc_region(r)) {
_cset_updater.add_region(r);
}
}
return false;
}
在上面代码①中,分区被标记,说明可以加入回收。这个判定的依据是分区在标记结束之前是否分配对象。对于Eden、Survivor来说在YGC结束阶段时,它们的标记起始位置设置为分区的bottom,对于老生代,它们的标记位置设置为分区的top。这一部分的逻辑散落在很多地方,分析源码的时候可以通过_prev_top_at_mark_start和_next_top_at_mark_start的变化来追踪。所以这里Eden、Survivor和一些空白的老生代分区不会加入到CollectionSetChooser中。大对象分区的连续分区也不会加入,正在被分配对象的老生代分区也不会加入。早期的代码在通过if (!r->isHumongous() && !r->is_young())直接过滤,可读性更高一些,现在的代码需要理解整个SATB算法实现的一些细节。
在上面代码②中,这个分区能否被加入到CSet Chooser中还有一个额外的参数G1MixedGCLiveThresholdPercent(默认值85),用于控制大对象不会加入到CSet(大对象在reclaim中处理),活跃对象占比应小于G1MixedGCLiveThresholdPercent。
在老生代回收处理的最后,还有一步,调用_collectionSetChooser->sort_regions()对CollectionSetChooser中的分区进行排序,排序的依据是根据每个分区的有效性。分区的有效性取决于两点:可回收的字节数以及回收的预测速度。有效性计算的代码如下所示:
hotspot/src/share/vm/gc_implementation/g1/heapRegion.cpp
void HeapRegion::calc_gc_efficiency() {
G1CollectedHeap* g1h = G1CollectedHeap::heap();
G1CollectorPolicy* g1p = g1h->g1_policy();
double region_elapsed_time_ms = g1p->predict_region_elapsed_time_ms(this,
false /* for_young_gc */);
_gc_efficiency = (double) reclaimable_bytes() / region_elapsed_time_ms;
}
启动混合收集
在并发标记结束后,需要通过g1_policy->record_concurrent_mark_cleanup_completed()设置标记,在下一次增量收集的时候,会判断是否可以开始混合收集。判断的依据主要是根据CSet中可回收的分区信息。是否可以启动混合收集的两个前提条件是:
- 并发标记已经结束,更新好CSet Chooser,用于下一次CSet的选择。
- YGC结束,判断是否可以进行混合收集。判断的依据在next_gc_should_be_mixed中。
启动混合收集的代码如下:
src/share/vm/gc_implementation/g1/g1CollectorPolicy.cpp
bool G1CollectorPolicy::next_gc_should_be_mixed(
const char* true_action_str, const char* false_action_str) {
CollectionSetChooser* cset_chooser = _collectionSetChooser;
if (cset_chooser->is_empty()) return false;
// 参数G1HeapWastePercent的默认值5,即当CSet Chooser中可回收的空间占总空间的
// 比例大于G1HeapWastePercent才会开始混合收集。
size_t reclaimable_bytes = cset_chooser->remaining_reclaimable_bytes();
double reclaimable_perc = reclaimable_bytes_perc(reclaimable_bytes);
double threshold = (double) G1HeapWastePercent;
if (reclaimable_perc <= threshold) return false;
return true;
}
在下一次分配失败,需要GC的时候会开始混合回收,这部分代码和我们前面提到的YGC的收集完全一致,唯一不同的就是CSet的处理。在真正的回收时候,会根据预测时间来选择收集的分区,其主要代码在G1CollectorPolicy::finalize_cset中,如下所示:
src/share/vm/gc_implementation/g1/g1CollectorPolicy.cpp
void G1CollectorPolicy::finalize_cset(double target_pause_time_ms,
EvacuationInfo& evacuation_info) {
……
// 所有的Eden和Survivor分区都需要收集
uint survivor_region_length = young_list->survivor_length();
uint eden_region_length = young_list->length() - survivor_region_length;
init_cset_region_lengths(eden_region_length, survivor_region_length);
……
// 这次收集是混合回收,这个if条件就是我们前面提到的两个条件,要满足这两个条件才能执行
if (!gcs_are_young()) {
CollectionSetChooser* cset_chooser = _collectionSetChooser;
// 获得老生代分区最小和最大处理数
const uint min_old_cset_length = calc_min_old_cset_length();
const uint max_old_cset_length = calc_max_old_cset_length();
bool check_time_remaining = adaptive_young_list_length();
HeapRegion* hr = cset_chooser->peek();
while (hr != NULL) {
// 老生代处理数达到最大值,停止添加CSet
if (old_cset_region_length() >= max_old_cset_length) break;
// 低于最小浪费空间G1HeapWastePercent可以停止添加CSet
if (reclaimable_perc <= threshold) break;
double predicted_time_ms = predict_region_elapsed_time_ms(hr, gcs_
are_young());
if (check_time_remaining) {
if (predicted_time_ms > time_remaining_ms) {
// 支持动态调整的分区设置,且预测时间超过目标停止时间,到达最小收集数则
// 停止添加CSet
if (old_cset_region_length() >= min_old_cset_length) break;
// 支持动态调整的分区设置,且预测时间超过目标停止时间,但是老生代收集数还没有
// 到达最小收集数,继续添加分区到CSet,同时记录有多少个分区超过这个目标时间
expensive_region_num += 1;
}
} else {
// 不支持动态调整的分区设置,只要到达最小收集数则停止添加CSet,所以不要指定
// 固定新生代大小
if (old_cset_region_length() >= min_old_cset_length) break;
}
// 把分区加入到CSet.
cset_chooser->remove_and_move_to_next(hr);
_g1->old_set_remove(hr);
add_old_region_to_cset(hr);
hr = cset_chooser->peek();
}
if (expensive_region_num > 0) {
// 输出一些信息,表示有多少个分区还没有达到最小值,但是可能已经超过预测时间。
……
}
}
……
}
以上代码涉及混合收集的部分是计算老生代最大和最小的分区收集数目。最小收集数的计算,代码如下所示:
hotspot/src/share/vm/gc_implementation/g1/g1CollectorPolicy.cpp
uint G1CollectorPolicy::calc_min_old_cset_length() {
const size_t region_num = (size_t) _collectionSetChooser->length();
const size_t gc_num = (size_t) MAX2(G1MixedGCCountTarget, (uintx) 1);
size_t result = region_num / gc_num;
// 取上限,表示最少收集一个老生代分区
if (result * gc_num < region_num) {
result += 1;
}
return (uint) result;
}
最小收集数计算的时候用到了一个参数G1MixedGCCountTarget(默认值为8),这个参数越大,收集老生代的分区越少,反之收集的分区越多。期望混合回收中,老生代分区在CSet中的比例超过1/G1MixedGCCountTarget。如果没有超过这个值,即便是预测时间超过了目标时间,仍然会添加;如果预测时间超过了目标时间,到达最小值之后就不会继续添加。
最大收集数的计算,代码如下所示:
hotspot/src/share/vm/gc_implementation/g1/g1CollectorPolicy.cpp
uint G1CollectorPolicy::calc_max_old_cset_length() {
G1CollectedHeap* g1h = G1CollectedHeap::heap();
const size_t region_num = g1h->num_regions();
const size_t perc = (size_t) G1OldCSetRegionThresholdPercent;
// G1OldCSetRegionThresholdPercent参数默认值是10,即以此最多收集10%的分区。
size_t result = region_num * perc / 100;
// 取上限
if (100 * result < region_num * perc) {
result += 1;
}
return (uint) result;
}
并发标记算法演示
混合回收中的垃圾回收阶段的步骤和YGC完全一致,本章不再介绍垃圾回收相关的步骤,这里仅仅介绍并发标记算法的步骤。
初始标记子阶段
初始标记是借助YGC阶段完成,这里我们仅仅关心和并发标记相关的部分。
如下图中,F-Free Region;S-Survivor Region;E-Eden Region;Old-Old Region。
在YGC阶段时,首先判定是否需要进行并发标记,如果需要,在对象复制阶段当发现有根集合直接到老生代的引用,那么这些对象会在YGC阶段被标记,如上图中nextMarkBitmap所示。
根扫描子阶段
根扫描主要是针对Survivor分区进行处理,所有的Survivor对象都将被认为是老生代的根,如图6-11所示。注意这一阶段仅仅对Survivor里面的对象标记,而不会处理对象的field。下图中深色区域是新增的活跃对象对应的区域。
注意这一阶段仅仅对Survivor里面的对象标记,而不会处理对象的field。上图中深色区域是新增的活跃对象对应的区域。
并发标记子阶段
并发标记子阶段是并发执行的,主要处理SATB队列,然后选择分区,根据nextMarkBitmap中已经标记的信息,对标记对象的每一个field指向的对象递归地进行标记。
这里的并发标记子阶段指的是,每个并发标记线程从全局的指针开始抢占分区(其实就是使用CAS指令进行串行处理),所有线程停止的条件就是所有的分区处理完毕。
在下图中SATB队列的处理可能会涉及所有的分区,然后根据分区递归处理已经标记的对象的Field,直到所有的分区处理完毕。
在并发标记子阶段中如果待标记对象过多,可能导致标记栈溢出,这个时候会再次循环处理根标记和并发标记子阶段。
再标记子阶段
再标记子阶段是并行执行的,主要是处理SATB,如下图所示。
再标记子阶段结束之后整个分区都已经完成了标记。
清理子阶段
在清理子阶段主要的事情有:分区计数,如果有空的老分区或者大对象分区,则释放,如果RSet处理中进行了扩展,则回收空间;把Old分区加入CSet Chooser。
并发标记的结果其实就是把垃圾比较多的老生代分区加入到CSet Chooser,那么标记的时候为什么要对整个堆的所有分区逐一标记?实际上是为了正确性,如果并发标记不处理新生代,可能导致老生代的活跃对象被误标记。清理阶段结束状态如下图所示。
GC活动图
到目前为止,我们介绍了Refine线程、YGC线程,在本章中还涉及并发线程。我们知道并发标记是依赖于YGC,即并发标记发生前一定有一次YGC。在并发标记结束之后,会更新CSet Chooser,此时如果在发生GC,则判断是否能够进行混合GC,混合GC的条件是上次发生的YGC不包含初始标记,并且CSet Chooser包含有效的分区。如果符合条件混合GC就会发生,注意混合GC不一定能在一次GC操作中完成所有的待收集的分区,所以混合GC可能发生多次,直到CSet Chooser中没有分区为止。下图是GC整体的活动图。
活动图中仅涉及Refine线程、并发标记线程、GC线程和Mutator;实际上如果打开字符串去重(见第9章),还有字符串去重线程;另外这里的Mutator指的是一般的解释和编译线程,如果是执行本地代码的线程处理还有所不同,具体可参考第10章的内容。
其中黑色箭头表示Refine线程,可以看到这些线程会一直运行(除了被GC线程中断)。双线空心箭头是并发标记线程,只有在YGC发生,且并发条件IHOP满足之后,才会开始执行,并发标记中有两个STW阶段:再标记和清理子阶段。浅灰色箭头表示的是Mutator运行。
日志解读
从一个Java的例子出发,代码如下:
public class Test {
private static final LinkedList<String> strings = new LinkedList<>();
public static void main(String[] args) throws Exception {
int iteration = 0;
while (true) {
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 10; j++) {
strings.add(new String("String " + j));
}
}
Thread.sleep(100);
}
}
}
运行程序使用的参数为:
-Xmx256M -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:G1LogLevel=finest
-XX:+PrintGCTimeStamps
我们截取了和并发标记相关的日志。并发标记的初始标记借用了YGC,所以在初始标记日志,能看到GC pause(G1 Evacuation Pause)(young)(initial-mark)这样的信息,说明这次的YGC发生后,就会开始并发标记。
100.070: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0751469 secs]
[Parallel Time: 74.7 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 100070.4, Avg: 100070.5, Max: 100070.6, Diff:
0.1]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum:
1.6]
[Update RS (ms): Min: 0.6, Avg: 1.1, Max: 1.5, Diff: 0.9, Sum: 8.9]
[Processed Buffers: Min: 1, Avg: 1.6, Max: 4, Diff: 3, Sum: 13]
[Scan RS (ms): Min: 1.0, Avg: 1.4, Max: 1.9, Diff: 0.9, Sum: 10.8]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum:
0.0]
[Object Copy (ms): Min: 71.5, Avg: 71.5, Max: 71.6, Diff: 0.1, Sum: 572.1]
[Termination (ms): Min: 0.3, Avg: 0.3, Max: 0.4, Diff: 0.1, Sum: 2.6]
[Termination Attempts: Min: 1382, Avg: 1515.5, Max: 1609, Diff: 227,
Sum: 12124]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
[GC Worker Total (ms): Min: 74.5, Avg: 74.5, Max: 74.6, Diff: 0.1, Sum:
596.3]
[GC Worker End (ms): Min: 100145.1, Avg: 100145.1, Max: 100145.1, Diff:
0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.4 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 23.0M(23.0M)->0.0B(14.0M) Survivors: 4096.0K->4096.0K Heap: 84.5M
(128.0M)->86.5M(128.0M)]
[Times: user=0.63 sys=0.00, real=0.08 secs]
并发标记日志信息:
// 把YHR中Survivor分区作为根,开始并发标记根扫描
100.146: [GC concurrent-root-region-scan-start]
// 并发标记根扫描结束,花费了0.0196297,注意扫描和Mutator是并发进行,同时有多个线程并行
100.165: [GC concurrent-root-region-scan-end, 0.0196297 secs]
// 开始并发标记子阶段,这里从所有的根引用:包括Survivor和强根如栈等出发,对整个堆进行标记
100.165: [GC concurrent-mark-start]
// 标记结束,花费0.08848s
100.254: [GC concurrent-mark-end, 0.0884800 secs]
// 这里是再标记子阶段,包括再标记、引用处理、类卸载处理信息
100.254: [GC remark 100.254: [Finalize Marking, 0.0002228 secs] 100.254:
[GC ref-proc, 0.0001515 secs] 100.254: [Unloading, 0.0004694 secs],
0.0011610 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
// 清除处理,这里的清除仅仅回收整个分区中的垃圾
// 这里还会调整RSet,以减轻后续GC中RSet根的处理时间
100.255: [GC cleanup 86M->86M(128M), 0.0005376 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
混合回收Mixed GC其实和YGC的日志类似,能看到GC pause(G1EvacuationPause)(mixed)这样的信息,日志分析参考YGC。
122.132: [GC pause (G1 Evacuation Pause) (mixed), 0.0106092 secs]
[Parallel Time: 9.8 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 122131.9, Avg: 122132.0, Max: 122132.0,
Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.7]
[Update RS (ms): Min: 0.5, Avg: 0.7, Max: 0.9, Diff: 0.4, Sum: 5.4]
[Processed Buffers: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 14]
[Scan RS (ms): Min: 1.0, Avg: 1.3, Max: 1.5, Diff: 0.5, Sum: 10.4]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum:
0.0]
[Object Copy (ms): Min: 7.5, Avg: 7.6, Max: 7.7, Diff: 0.2, Sum: 60.9]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[Termination Attempts: Min: 92, Avg: 105.1, Max: 121, Diff: 29, Sum: 841]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 9.7, Avg: 9.7, Max: 9.8, Diff: 0.1, Sum: 77.6]
[GC Worker End (ms): Min: 122141.7, Avg: 122141.7, Max: 122141.7, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.2 ms]
[Other: 0.7 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.5 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 3072.0K(3072.0K)->0.0B(5120.0K) Survivors: 3072.0K->1024.0K
Heap: 105.5M(128.0M)->104.0M(128.0M)]
[Times: user=0.00 sys=0.00, real=0.01 secs]
通过打开选项G1SummarizeConcMark,可以看到并发标记的概述信息:
Concurrent marking:
// 格式 多少个历史数据 类型
0 init marks: total time = 0.00 s (avg = 0.00 ms).
5 remarks: total time = 0.02 s (avg = 4.76 ms).
// remark有5个历史数据,所以给出了均值、最大值
[std. dev = 4.45 ms, max = 13.24 ms]
5 final marks: total time = 0.02 s (avg = 3.14 ms).
[std. dev = 4.25 ms, max = 11.17 ms]
5 weak refs: total time = 0.01 s (avg = 1.62 ms).
[std. dev = 0.30 ms, max = 2.07 ms]
5 cleanups: total time = 0.01 s (avg = 1.27 ms).
[std. dev = 0.23 ms, max = 1.70 ms]
// 这是在并发标记的最后,对分区对象进行计数的时间花费
Final counting total time = 0.00 s (avg = 0.36 ms).
// 这是处理RSet的花费
RS scrub total time = 0.00 s (avg = 0.39 ms).
// 这个时间是初始化、再标记、清理的时间
Total stop_world time = 0.03 s.
// 并发线程的总花费和标记时间花费
Total concurrent time = 1.53 s ( 1.53 s marking).
打开G1PrintRegionLivenessInfo,打印每个堆的使用信息。主要包括两个部分,第一部分是在并发标记后,所有分区的情况,包括Eden、Old、Survivor和Free。第二部分是在CSet选择时需要对老生代分区进行排序,排序的依据是gc-eff,这个值越大说明回收该分区的效率越高,所以会优先选择效率高的老生代分区。
### PHASE Post-Marking @ 278.046
### HEAP reserved: 0x00000000f0000000-0x0000000100000000 region-size:
1048576
//描述是整体堆的信息,起止地址以及分区的大小
###
### type address-range used prev-live
next-live gc-eff remset code-roots
//这是输出信息的头部,它告诉我们关于区域的类型、区域的寻址范围、区域的已用空间、相对于之前
//标记周期的区域的活跃数据、标记后活跃数据空间、区域的GC效率、RSet大小、CodeRoot大小。
### (bytes) (bytes) (bytes)
(bytes/ms) (bytes) (bytes)
### OLD 0x00000000f0000000-0x00000000f0100000 1048576 1044480
1044480 0.0 3952 16
//这个OLD表示老生代,该分区的地址范围,已经使用的内存为1MB,标记之前和之后都是1M,说
//明该分区在清理阶段没有任何处理,效率还没计算为0,RSet大小为3952B,Code为16B,实际
//上这里的16是数据结构占用的大小,而没有实际的内容
……
### FREE 0x00000000fe300000-0x00000000fe400000 0 0
0 0.0 2920 16
……
### SURV 0x00000000ff500000-0x00000000ff600000 1048576 1048576
1048576 0.0 3952 16
### EDEN 0x00000000fff00000-0x0000000100000000 420024 420024
420024 0.0 2920 16
###
### SUMMARY capacity: 256.00 MB used: 229.40 MB / 89.61 % prev-live:
215.72 MB / 84.27 % next-live: 211.63 MB / 82.67 % remset: 1.21 MB
code-roots: 0.01 MB
### PHASE Post-Sorting @ 278.048
### HEAP reserved: 0x00000000f0000000-0x0000000100000000 region-size:
1048576
###
### type address-range used prev-live
next-live gc-eff remset code-roots
### (bytes) (bytes) (bytes)
(bytes/ms) (bytes) (bytes)
### OLD 0x00000000f5f00000-0x00000000f6000000 1048576 104168
0 710320.5 3952 16
### OLD 0x00000000f8000000-0x00000000f8100000 1048576 170584
0 659749.2 3608 16
……
### OLD 0x00000000fc000000-0x00000000fc100000 1048576 830672
0 44549.9 4640 16
###
### SUMMARY capacity: 38.00 MB used: 38.00 MB / 100.00 % prev-live:
21.67 MB / 57.02 % next-live: 0.00 MB / 0.00 % remset: 0.21 MB code-
roots: 0.00 MB
229M->229M(256M), 0.0028795 secs]
参数优化
本章着重介绍了并发标记,本节总结并发标记和垃圾回收中涉及的相关参数,介绍参数的意义以及该如何使用这些参数:
- 参数InitiatingHeapOccupancyPercent(简称为IHOP),默认值为45,这个值是启动并发标记的先决条件,只有当老生代内存占总空间45%之后才会启动并发标记任务。增加该值,将导致并发标记可能花费更多的时间,也会导致YGC或者混合GC收集时收集的分区变少,但另一方面就有可能导致FGC。根据经验这个值通常根据整体应用占用的平均内存来设置,可以把该值设置得比平均内存稍高一些,此时性能最好(即YGC/混合GC比较快,且FGC比较少)。那么如何得到应用程序在运行时的内存使用情况?可以打开G1PrintHeapRegions观察内存的分配和使用情况,另外JVM提供了一个诊断选项G1PrintRegionLivenessInfo,打开该选项,可以查看到内存的使用情况。IHOP的设置非常有用,但是设置合理的IHOP并不容易,需要不断地尝试。[插图]
- 参数G1ReservePercent,默认值为10,当发现GC晋升失败导致FGC,可以增大该值。
- 参数ConcGCThreads为并发线程数,默认值为0,如果没有设置则动态调整;使用ParallelGCThreads(前文介绍过推断依据)为依据来推断。ConcGCThreads =(ParallelGCThreads+2)/4,最小值为1,如果发现并发标记耗时较多可以增大该值,注意增大该值会导致Mutator执行的吞吐量变小。
- 参数HeapSizePerGCThread,默认值为64M,可以简单地理解为每64M分配一个线程。
- 参数UseDynamicNumberOfGCThreads,默认为false,打开该值表示可以动态调整线程数;调整的依据会根据最大线程数、HeapSizePerGCThread等确定。
- 参数ForceDynamicNumberOfGCThreads,默认为false,打开该值表示可以动态调整,和UseDynamicNumberOfGCThreads功能类似。
- 参数G1SATBBufferSize,默认值为1K,表示每个STAB队列最多存放1000个灰色对象,注意这里不是SATB queue set的大小。
- 参数G1SATBBufferEnqueueingThresholdPercent(默认值是60),表示当一个队列满了之后,首先进行过滤处理,过滤后如果使用率超过这个阈值把队列送入到queue set并新分配一个队列。·参数MarkStackSize和MarkStackSizeMax,在32位JVM中设置为32k和4M,64位JVM中设置为4M和512M。如果没有设置可以启发式推断参数,确保MarkStackSize最小为32k(或者和并发线程参数ParallelGCThreads正相关,如ParallelGCThreads=8,则32位JVM中MarkStackSize=8×16k=128K,其中16k是队列的大小),这个参数是并发标记子阶段中用到的标记栈的大小。
- 参数GCDrainStackTargetSize,默认值为64,表示并发标记子阶段处理时为了保证处理的性能,一次标记的最多对象个数。
- 参数G1MixedGCLiveThresholdPercent,默认值85,用于判断分区能否被加入到CSet中,低于该值将会被加入。
- 参数G1HeapWastePercent,默认值5,即当CSet中可回收空间的占总空间的比例大于G1HeapWastePercent才会开始混合收集。
- 参数G1MixedGCCountTarget,默认值为8,这个参数越大,收集老生代的分区越少,反之收集的分区越多。要保持老生代分区在CSet中的比例超过1/G1MixedGCCountTarget。
- 参数G1OldCSetRegionThresholdPercent,参数默认值是10,即一次最多收集10%的分区。
- 参数G1ConcMarkStepDurationMillis,默认值为10,表示每个并发标记子阶段每次最多执行10ms。
- 参数G1UseConcMarkReferenceProcessing,默认值为true,打开表示在并发标记的时候可以标记引用。
- 在打开引用处理时,每次标记处理引用的对象数由G1RefProcDrainInterval控制,默认值为10。
- 参数ClassUnloadingWithConcurrentMark,默认值为true,打开表示在并发标记的时候可以卸载已经加载的类。