3

你必须明白的新生代垃圾回收:YoungGC

 3 years ago
source link: https://my.oschina.net/u/4472036/blog/5036898
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

关注公众号:Java架构师联盟,每日更新技术好文

Young GC

前文提到,Young GC(以下简称YGC)是指新生代垃圾回收,下面将详细讨论G1的YGC过程。

选择CSet

YGC的回收过程位于 G1CollectedHeap::do_collection_pause_at_safepoint(),在进行垃圾回收前它会创建一个清理集CSet(Collection Set),存放需要被清理的Region。选择合适的Region放入CSet是为了让G1达到用户期望的合理的停顿时间。CSet的创建过程如代码清单11-2所示:

代码清单11-2 选择Region放入CSet

void G1Policy::finalize_collection_set(...) {
// 先选择新生代Region,用户期望的最大停顿时间是target_pause_time_ms
// G1计算出清理新生代Region的可能用时后,会将剩下的时间(time_remaining_ms)给老年代
double time_remaining_ms =
_collection_set->finalize_young_part(...);
_collection_set->finalize_old_part(time_remaining_ms);
}

G1的YGC只负责清理新生代Region,因此finalize_old_part()不会选择任何Region,所以只需要关注finalize_young_part()。finalize_young_part会在将所有Eden和Survivor Region加入CSet后准备垃圾回收。

G1在evacuate_collect_set()中创建G1ParTask,然后阻塞,直到G1ParTask执行完成,这意味着整个YGC期间应用程序是STW的。类似Parallel GC的YGC,G1ParTask的执行由线程组GangWorker完成,以尽量减少STW时间。不难看出,YGC的实际工作位于G1ParTask,它主要分为三个阶段:

1)清理根集( G1RootProcessor::evacuate_roots);

2)处理RSet( G1RemSet::oops_into_collection_set_do);

3)对象复制( G1ParEvacuateFollowersClosure::do_void)。

第一阶段是清理根集。第10章提到HotSpot VM很多地方都属于GCRoot,G1ParTask的evacuate_roots()会从这些GC Root出发寻找存活对象。以线程栈为例,G1会扫描虚拟机所有JavaThread和VMThread的线程栈中的每一个栈帧,找到其中的对象引用,并对它们应用G1ParCopyClosure,如代码清单11-3所示:

代码清单11-3 G1ParCopyClosure

void G1ParCopyClosure<barrier, do_mark_object>::do_oop_work(T* p) {
...
oop obj = CompressedOops::decode_not_null(heap_oop);const InCSetState state = _g1h->in_cset_state(obj);
// 如果对象属于CSet
if (state.is_in_cset()) {
oop forwardee;
markOop m = obj->mark_raw();
if (m->is_marked()) { // 如果已经复制过则直接返回复制后的新地址
forwardee = (oop) m->decode_pointer();
} else { // 将它复制到Survivor Region,返回新地址
forwardee = _par_scan_state->copy_to_survivor_space(...);
}
// 修改根集中指向该对象的引用,指向Survivor中复制后的对象
RawAccess<IS_NOT_NULL>::oop_store(p, forwardee);
...
} else {
...
}
}

清理根集的核心代码是copy_to_survivor_space,它将Eden Region中年龄小于15的对象移动到Survivor Region,年龄大于等于15的对象移动到Old Region。之前根集中的引用指向Eden Region对象,对这些引用应用G1ParCopyClosure之后,Eden Region的对象会被复制到SurvivorRegion,所以根集的引用也需要相应改变指向,如图11-3所示。

img

图11-3 清理根集

copy_to_survivor_space在移动对象后还会用G1ScanEvacuatedObjClosure处理对象的成员,如果成员也属于CSet,则将它们放入一个G1ParScanThreadState队列,等待第三阶段将它们复制到Survivor Region。总结来说,第一阶段会将根集直接可达的对象复制到Survivor Region,并将这些对象的成员放入队列,然后更新根集指向。

处理RSet

第一阶段标记了从GC Root到Eden Region的对象,对于从OldRegion到Eden Region的对象,则需要借助RSet,这一步由G1ParTask的 G1RemSet::oops_into_collection_set_do完成,它包括更新RSet(update_rem_set)和扫描RSet(scan_rem_set)两个过程。

scan_rem_set遍历CSet中的所有Region,找到引用者并将其作为起点开始标记存活对象。

经过前面的步骤后,YGC发现的所有存活对象都会位于G1ParScanThreadState队列。对象复制负责将队列中的所有存活对象复制到Survivor Region或者晋升到Old Region,如代码清单11-4所示:

代码清单11-4 对象复制

template <class T> void G1ParScanThreadState::do_oop_evac(T* p) {
// 只复制位于CSet的存活对象
oop obj = RawAccess<IS_NOT_NULL>::oop_load(p);
const InCSetState in_cset_state = _g1h->in_cset_state(obj);
if (!in_cset_state.is_in_cset()) {
return;
}
// 将对象复制到Survivor Region(或晋升到Old Region)
markOop m = obj->mark_raw();
if (m->is_marked()) {
obj = (oop) m->decode_pointer();
} else {
obj = copy_to_survivor_space(in_cset_state, obj, m);
}
RawAccess<IS_NOT_NULL>::oop_store(p, obj);
// 如果复制后的Region和复制前的Region相同,直接返回
if (HeapRegion::is_in_same_region(p, obj)) {
return;}
// 如果复制前Region是老年代,现在复制到Survivor/Old Region,
// 则会产生跨代引用,需要更新RSet
HeapRegion* from = _g1h->heap_region_containing(p);
if (!from->is_young()) {
enqueue_card_if_tracked(p, obj);
}
}

对象复制是YGC的最后一步,在这之后新生代所有存活对象都被移动到Survivor Region或者晋升到Old Region,之前的Eden空间可以被回收(Reclaim)。另外,YGC复制算法相当于做了一次堆碎片的清理工作,如整理Eden Region可能存在的碎片。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK