写点什么

深入解析 java 虚拟机:垃圾回收,最大并发标记清除垃圾回收器

用户头像
极客good
关注
发布于: 刚刚

CMS GC 线程会进入一个循环,每次它调用 sleepBeforeNextCycle()时会阻塞一段时间,唤醒后使用


CMSCollector::collect_in_background()清理老年代,如代码清单 10-17 所示:


代码清单 10-17 collect_in_background


void CMSCollector::collect_in_background(GCCause::Cause cause) {


...


switch (_collectorState) {


// 初始标记(STW)


case InitialMarking:{ReleaseForegroundGC x(this);


stats().record_cms_begin();


VM_CMS_Initial_Mark initial_mark_op(this);


VMThread::execute(&initial_mark_op);


}


break;


// 并发标记


case Marking: markFromRoots();break;


// 预清理


case Precleaning: preclean();break;


// 可中断预清理


case AbortablePreclean: abortable_preclean();break;


// 重新标记(STW)


case FinalMarking:{


ReleaseForegroundGC x(this);


VM_CMS_Final_Remark final_remark_op(this);


VMThread::execute(&final_remark_op);


}


break;


// 并发清理


case Sweeping: sweep(); // fallthrough


case Resizing: {


ReleaseForegroundGC x(this);


MutexLockerEx y(...);


CMSTokenSync z(true);


if (_collectorState == Resizing) {


compute_new_size();save_heap_summary();


_collectorState = Resetting;


}


break;


}


// 重置垃圾回收器的各种数据结构


case Resetting: ... break;


case Idling:


default: ShouldNotReachHere();break;


}


...


}


collect_in_background 实现了一个完整的 Old GC,代码使用状态机模式,通过_collectorState 状态转换来切换到不同的垃圾回收周期,简化了代码逻辑。


1. 初始标记


初始标记(InitiaMarking)是 Old GC 的第一个周期,它需要 Mutator 线程暂停,这一步通过安全点来保障,而虚拟机中能开启安全点的操作只能是 VMThread,所以 InitialMarking 阶段会创建一个 VM_CMS_Initial_Mark 的 VMOperation,当 VMThread 执行该 VMOperation 并协调所有线程进入安全点后,会调用


checkpointRootsInitialWork()进行初始标记,如代码清单 10-18 所示:


代码清单**10-18


chekcpointRootsInitialWork**


void CMSCollector::checkpointRootsInitialWork() {


// 确保位于安全点,并且处于 InitialMarking 阶段


assert(SafepointSynchronize::is_at_safepoint(), ...);


assert(_collectorState == InitialMarking, "just checking");


...


// 新生代指向老年代的引用


MarkRefsIntoClosure notOlder(_span, &_markBitMap);


...


if (CMSParallelInitialMarkEnabled) {


... // 使用多线程进行初始标记


CMSParInitialMarkTask tsk(this, &srs, n_workers);


if (workers->total_workers() > 1) {


workers->run_task(&tsk);


} else {


tsk.work(0);


}


} else {


... // 使用单线程进行初始标记


heap->cms_process_roots(...,?Older, &cld_closure);


}


...


}


代码清单 10-18 说明了并发和并行并不是互斥的概念,并发标记清除把整个标记清除细分为几个阶段,然后以 STW 的方式执行其中两个阶段,其他阶段允许 Mutator 线程和 GC 一起工作,在 STW 的两个阶段,垃圾回收器还可以充分发挥多核处理器的优势,使用多个线程进行回收工作,减少 STW 时间。


为了进一步减少 STW 时间,初始标记只会扫描并标记 GC Root 指向老年代的直接引用以及新生代指向老年代的直接引用,而所有间接引用都由后面的并发标记处理。


2. 并发标记


初始标记是从 GC Root 和新生代指向老年代记忆集出发,寻找直接可达的对象,接下来并发标记(Marking)是从这些对象出发,寻找间接可达的对象。


这一步由 markFromRoots()完成,该函数内部会创建 CMSConcMarkingTask 并发标记。CMSConcMarkingTask 包括标记逻辑和工作窃取逻辑,前者由 do_scan_and_mark 完成,后者由 do_work_stealing 完成。


标记的逻辑是每当发现初始标记的存活对象 cur,就将它放入_markStack,然后进入循环。每次从_markStack 中弹出一个对象,扫描 cur 的成员引用,直到_markStack 为空,这是一个典型的广度优先搜索过程,只是 CMS GC 在扫描 cur 成员引用时稍有改变,它不会将扫描到的 cur 的成员全部放入_markStack,而是选择性地放入,如图 10-10 所示。



图 10-10 BFS 过程中处理 cur 对象的成员引用


假设 cur 表示对象 C。对象 C 有成员对象 A 和 E,A 的地址位于 C 的前面,垃圾回收器会标记 A,并扫描 A 的成员引用 B;B 的地址位于 C 前面,标记 B 并扫描 B 的成员引用 D;D 的地址位于 C 后面,只标记 D,将 D 的成员放入_markStack 但是不继续扫描(本例中对象 D 没有成员);接着处理对象 E,E 地址位于 C 后面,所以只标记不扫描它的成员引用。


总结来说,扫描策略是找到存活对象 cur,如果它的成员对象地址位于 cur 前面,则标记并继续扫描成员对象,如果它的成员对象地址位于 cur 后面,则只标记不扫描成员对象。这样做实际上结合了广度优先搜索和深度优先搜索,好处是减小了_markStack 的大小,在该例中_markStack 最大仅包含一个元素,若直接使用广度优先搜索会导致_markStack 快速膨胀,虚拟机内存空间不足的情况。


3. 预清理


并发预清理和并发可中断预清理(Precleaning &&AbortablePreclean)是可选步骤,如果关闭


-XX:-CMSPrecleaningEnabled,虚拟机会跳过它直接执行下一阶段的重新标记。


如果上一阶段并发标记过程中 Mutator 线程修改了对象引用关系,比如创建了新生代指向老年代的引用,那么预清理可以发现这些修改,并标记老年代的对象图。可中断预清理与之类似,它会尝试若干次预清理过程,直到次数到达 GC 允许的上限,或者超过指定时间。两个阶段的意义在于做尽可能多的标记工作,减少下一阶段重新标记的 STW 时间。


4. 重新标记


重新标记(FinalMarking)过程会再次停止全部 Mutator 线程(STW),只允许垃圾回收线程。


因为初始标记到重新标记的间隔允许 Mutaor 线程和 GC 线程一起进行,所以可能产生大量从新生代指向老年代的引用,即新生代记忆集大增,也可能之前新生代已经存活的很多对象变成了死亡对象,但是 GC 不知道这个事实,仍然从 GC Root 和新生代记忆集出发标记存活对象,使本该死亡的对象被标记为存活对象,产生浮动垃圾。这是分代垃圾回收器面临的常见问题,如果开启-XX:+CMSScavengeBeforeRemark,在重新标记前 GC 会先对新生代进行垃圾回收,这样可以有效减少新生代记忆集大小,继而减少重新标记造成的 STW 时间。注意,以上讨论仅在两次 STW 标记期间新生代记忆集大增,或者大量新生代记忆集的对象从存活转变为死亡时才成立,如果随意开启该选项可能适得其反。


除了重新标记新增可选的新生代回收步骤外,重新标记过程与初始标记过程大致一样,两者都是向 VMThread 投递 VMOperation,区别在于前者的 VMOperation 调用


checkpointRootsInitialWork,后者调用 checkpointRootsFinalWork,如代码清单 10-19 所示:


代码清单 10-19?重新标记


vo


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


id CMSCollector::checkpointRootsFinalWork() {


...


// 根据虚拟机参数使用多线程重新标记或者使用单线程重新标记


if (CMSParallelRemarkEnabled) {

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
深入解析java虚拟机:垃圾回收,最大并发标记清除垃圾回收器