写点什么

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

用户头像
小Q
关注
发布于: 2021 年 04 月 29 日

关注公众号: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);// 如果对象属于CSetif (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 所示。



图 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,// 则会产生跨代引用,需要更新RSetHeapRegion* 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 可能存在的碎片。

发布于: 2021 年 04 月 29 日阅读数: 161
用户头像

小Q

关注

还未添加个人签名 2020.06.30 加入

小Q 公众号:Java架构师联盟 作者多年从事一线互联网Java开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果能为您提供帮助,请给予支持(关注、点赞、分享)!

评论

发布
暂无评论
你必须明白的新生代垃圾回收:YoungGC