G1-垃圾回收简述(三)原创
Mixed GC回收流程介绍
前面3篇文章,分解了G1的分区(Region),RSet,以及新生代的回收也就是G1-YGC。而G1的混合回收,则是既回收新生代分区也回收部分老年代分区。回收新生代分区的我就不再提了,着重关注老年代的并发标记,并发清理。针对Mixed GC而言没主要分为标记清理和回收俩个阶段,我在这里逐步分析。
标记清理
- 初始标记
初始标记需要STW。和新生代的处理一样,也是从Java根或者JVM根出发进行标记,而且由于混合Mixed GC发生一定会触发一次YGC,所以这里的初始标记直接借用YGC收集的结果,也就是survivor的作为根。
- 并发标记
并发标记依赖于Survivor区和老年代的Rset进行并发标记,触发的时机在于YGC之后堆空间使用率达到InitiatingHeapOccupancyPercent==45的阈值,此时并发标记线程和应用线程同时进行,这时候就会可能标记的不准确,由此引出了G1的三色标记。G1使用快照的模式来保证宁可不杀,也不错杀的原则,等最后重标记阶段去兜底处理。即便是出现了
B.x=null;
也不直接处理,而是使用write barrier来处理,就是发生引用删除的时候,在执行出插入一块写屏障,把这个对象放在STAB当中,标记为活对象。这一块儿的处理呢,就会存在浮动垃圾. - 重标记
同样需要一个STW,用来最终确认整个标记的活跃对象。在三色标记中,G1保存了对象的初始快照,所以需要在STW的时候进行处理,
- 清理阶段
这个阶段特直观,G1只会回收全是垃圾的分区。重置老年代RSet
回收阶段
经过标记清理阶段之后,就是要把那些不全是活跃对象的空间进行复制,将其腾挪到空闲列表中,回收不全为空的分区。这个阶段和YGC一样,从分区中选取N个分区当作CSet。然后把这些分区中的活跃对象拷贝到空闲的分区之内,复制完毕之后,再把回收的分区驾到空闲分区当中。只不过YGC有特殊的是会有一步晋升的处理。
三色标记
三色标记算法用于在并发标记阶段防止漏标和错标的算法。定义了三种颜色,黑色,灰色,白色,
- 黑色
表示当前对象已近被标记到且当前对象的所有field所引用的对象都已经被标记
- 灰色
表示自己已经被标记,但是其Field还没有被处理
- 白色
表示自己还没有被处理。
从GCRoot出发标记对象,当前Mutator线程也是在工作状态,并行于并发标记线程,所以容易出现漏标。例如:Mutator线程把一个黑色对象的关联到一个白色对象,此时需要更新黑色对象,不更新就会导致白色对象会被漏标,还有就是Mutator线程把一个灰色对象到白色对象的引用删除了,由于灰色对象这是标记自身了,并没有标记到field,如果直接此时删除了这层关系,那么对应的引用关系就会被漏标。
解决漏标问题,有增量更新和STAB的方式来处理
- 增量更新
在CMS中使用,当对象引用发生改变是,把黑色对象重新标记为灰色,进行重新标记。
- STAB
G1则使用STAB缓存来解决,即在对象赋值之前,把对象开始的引用状态保存下来,处理STAB缓存。
上述两图中分别模拟了增量更新和STAB对待漏标的处理流程。从图中的GcRoot触发,标记到俩个对象(A,B),其中(黑色)A对象自身的其所有的field引用都以被标记过。(灰色)B对象自身已被标记完毕,其field的引用在标记队列中进行标记过程中,没有全部标记完毕。(白色)C对象还没有被GcRoot关联到。注意此时的Mutator和并发标记线程是在同时进行的。当应用线程就改了A和B的引用关系。如去掉了A->B的引用,增加了A->C的引用,去掉了B->C,不处理就是发生漏标。
细心的你可能会发现有疑问的点,为什么STAB的方式,是把C对象标记为灰色。具体操作呢则是在应用线程发生删除应用关系的同时,先把B对象的引用的C对象放到标记队列当中,其实就相当于C对象标记为灰色。
写屏障
这里的写屏障不是并发访问中的内存屏障,STAB中的写屏障是主要用于保证并发标记过程的准确,在Rset中也有写屏障用于处理引用关系。
在对象field赋值阶段,JVM会进行如下方法调用oop.cpp#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(p, v);
oopDesc::encode_store_heap_oop(p, v);
// always_do_update_barrier == false =>
// Either we are at a safepoint (in GC) or CMS is not used. In both
// cases it's unnecessary to mark the card as dirty with release sematics.
update_barrier_set((void*)p, v, false /* release */); // cast away type
}
}
在调用oop_store方法的时候,前置处理就是把目标对象放在队列中,实际上执行的是入队方法,如下
G1SATBCardTableModRefBS.cpp#enqueue
void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
// Nulls should have been already filtered.
assert(pre_val->is_oop(true), "Error");
if (!JavaThread::satb_mark_queue_set().is_active()) return;
Thread* thr = Thread::current();
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);
}
}
这个STAB队列在并发标记线程中自旋消费。
在调用oop_store方法的时候,后置处理就是把目标对象放在DCQ当中,表示该对象有引用关系,供RSet处理。
Mixed GC源码分析
混合GC的开始,基于G1 YGC的完成,并发标记过程由并发标记线程从Survivor区或者老年代的RSet处记录的的活跃对象作为根开始标记,并发标记线程的执行逻辑主要在concurrentMarkThread.cpp#run
方法中.
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) {
sleepBeforeNextCycle();
if (_should_terminate) {
break;
}
{
ResourceMark rm;
HandleMark hm;
double cycle_start = os::elapsedVTime();
double scan_start = os::elapsedTime();
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]");
}
}
} while (cm()->restart_for_overflow());
double end_time = os::elapsedVTime();
_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 {
SuspendibleThreadSetJoiner sts;
g1h->set_marking_complete();
}
if (g1h->free_regions_coming()) {
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]");
}
_cm->completeCleanup();
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");
{
SuspendibleThreadSetJoiner sts;
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]");
}
}
if (!cm()->has_aborted()) {
SuspendibleThreadSetJoiner sts;
_cm->clearNextBitmap();
} else {
assert(!G1VerifyBitmaps || _cm->nextMarkBitmapIsClear(), "Next mark bitmap must be clear");
}
}
{
SuspendibleThreadSetJoiner sts;
g1h->increment_old_marking_cycles_completed(true /* concurrent */);
g1h->register_concurrent_cycle_end();
}
}
assert(_should_terminate, "just checking");
terminate();
}
并发标记线程的任务在创建后也不是直接启动,并发标记的启动依赖于YGC,在YGC的最后阶段会嗲用doConcurrentMark方法来启动。判断启动的条件在于当老年代使用的内存加上本次即将分配的内存占到总内存的45%,就表明需要启动混合GC,然后就要启动并发线程。
观察并发标记线程的执行方法。
- 扫描根进行标记参见
scanRootRegions
方法 - 并发标记参见
markFromRoots
方法 - 最终标价阶段处理参见
CMCheckpointRootsFinalClosure
闭包 - 循环处理针对栈空间溢出
- 清理阶段,回收
后序具体源码细节 后序补上,🐶困得不行了