性能文章>Shenandoah GC:一个来自OpenJDK12的全新并发压缩垃圾回收器>

Shenandoah GC:一个来自OpenJDK12的全新并发压缩垃圾回收器原创

2年前
704300

是不是才听说了JDK11的ZGC,并且还没搞懂?不好意思,OpenJDK12马不停蹄的带来了Shenandoah GC

概述

JDK12新增的一个名为Shenandoah的GC算法,它的evacuation阶段工作能通过与正在运行中Java工作线程同时进行(即并发,concurrent),从而减少GC的停顿时间。

Shenandoah的停顿时间和堆的大小没有任何关系,这就意味着无论你的堆是200MB,2GB还是200GB,停顿时间是一样的。
image.png
如上图所示,Shenandoah GC每个GC周期由2个STW(Stop The World)阶段和2个并发阶段组成。在初始化标记阶段,扫描root集合的时候会STW。然后并发标记阶段,Shenandoah GC和Java工作线程一起运行,最后,在最终标记阶段,又会STW,然后执行一个并发evacuation阶段。

Root集合包括:thread local variables, references embedded in generated code, interned Strings, references from classloaders (e.g. static final references), JNI references, JVMTI references.
深入剖析

Shenandoah是一个基于Region设计的垃圾收集器,这点和G1类似,它把整个堆当作Region集合来维护。但是,Shenandoah不需要remember set或者card table来记录跨region引用。
其中一个原因是(无条件)card mark可能引起false sharing,而Brooks pointer分散在每个对象头上,比较不容易引起false sharing:
image.png
一个常规的Shenandoah GC周期大概是这样的(跟G1也有点相似):
image.png
GC日志如下:

GC(3) Pause Init Mark 0.771ms
GC(3) Concurrent marking 76480M->77212M(102400M) 633.213ms
GC(3) Pause Final Mark 1.821ms
GC(3) Concurrent cleanup 77224M->66592M(102400M) 3.112ms
GC(3) Concurrent evacuation 66592M->75640M(102400M) 405.312ms
GC(3) Pause Init Update Refs 0.084ms
GC(3) Concurrent update references  75700M->76424M(102400M) 354.341ms
GC(3) Pause Final Update Refs 0.409ms
GC(3) Concurrent cleanup 76244M->56620M(102400M) 12.242ms

每个阶段要做的事情如下:

Init Mark 并发标记的初始化阶段,它为并发标记准备堆和应用线程,然后扫描root集合。这是整个GC生命周期第一次停顿,这个阶段主要工作是root集合扫描,所以停顿时间主要取决于root集合大小。

Concurrent Marking 贯穿整个堆,以root集合为起点,跟踪可达的所有对象。 这个阶段和应用程序一起运行,即并发(concurrent)。这个阶段的持续时间主要取决于存活对象的数量,以及堆中对象图的结构。由于这个阶段,应用依然可以分配新的数据,所以在并发标记阶段,堆占用率会上升。

Final Mark 清空所有待处理的标记/更新队列,重新扫描root集合,结束并发标记。. 这个阶段还会搞明白需要被清理(evacuated)的region(即垃圾收集集合),并且通常为下一阶段做准备。最终标记是整个GC周期的第二个停顿阶段,这个阶段的部分工作能在并发预清理阶段完成,这个阶段最耗时的还是清空队列和扫描root集合。

Concurrent Cleanup 回收即时垃圾区域 – 这些区域是指并发标记后,探测不到任何存活的对象。

Concurrent Evacuation 从垃圾收集集合中拷贝存活的对到其他的region中,这是有别于OpenJDK其他GC主要的不同点。这个阶段能再次和应用一起运行,所以应用依然可以继续分配内存,这个阶段持续时间主要取决于选中的垃圾收集集合大小(比如整个堆划分128个region,如果有16个region被选中,其耗时肯定超过8个region被选中)。

Init Update Refs 初始化更新引用阶段,它除了确保所有GC线程和应用线程已经完成并发Evacuation阶段,以及为下一阶段GC做准备以外,其他什么都没有做。这是整个GC周期中,第三次停顿,也是时间最短的一次。

Concurrent Update References 再次遍历整个堆,更新那些在并发evacuation阶段被移动的对象的引用。这也是有别于OpenJDK其他GC主要的不同,这个阶段持续时间主要取决于堆中对象的数量,和对象图结构无关,因为这个过程是线性扫描堆。这个阶段是和应用一起并发运行的。

Final Update Refs 通过再次更新现有的root集合完成更新引用阶段,它也会回收收集集合中的region,因为现在的堆已经没有对这些region中的对象的引用。

这是整个GC周期最后一个阶段,它的持续时间主要取决于root集合的大小。

Concurrent Cleanup 回收那些现在没有任何引用的Region集合。

目标

Shenandoah不是一个要一统天下的GC,有一些其他的吞吐量优先,或者内存占用优先的GC算法,它们并不是把响应性放在第一位(即不是主要考虑缩短停顿时间)。

Shenandoah是一个对那些更看重响应性和可预测短暂停顿的应用来说,更合适的GC算法。它的目标不是要解决所有JVM的停顿问题,由于GC之外的其他原因(例如到达安全点时间(TTSP–Time To Safe Point)问题)而暂停时间超出了此JEP的范围。

现代服务器比以前拥有更多的内存和处理器,SLA应用需要保证RP在10~500ms。为了达到
最苛刻的目标(保证RP在10ms以内),我们需要GC的算法足够高效,允许程序在可用内存中运行,并且经过优化后,永远不会让正在运行的程序的停顿时间超过5毫秒(a handful of milliseconds,一只手就5根手指头,所以是5ms)。

Shenandoah就是这样一个OpenJDK为更近这个目标而设计的开源、低停顿时间的垃圾回收器。

替代方案

  1. Zing/Azul是一个没有停顿的垃圾收集器,但是不会贡献给OpenJDK。

  2. 基于colored pointers设计的ZGC也是一个拥有很低停顿时间的垃圾收集器,Shenandoah期望能与之一战。

  3. G1很多工作都是并行或者并发的,但是evacuation阶段不能并发执行。

  4. CMS能并发标记,但是它执行年轻代拷贝时,需要STW,并且不会压缩老年代,这就会导致花费更多时间来管理老年代中的可用空间以及碎片问题。

使用

这还是一个体验功能,需要增加-XX:+UnlockExperimentalVMOptions参数才能开启Shenandoah GC:

-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

常规测试

RedHat已经做了大量的测试,OpenJDK也为Shenandoah开发了很多测试用例。而且从Fedora 24开始Shenandoah在Fedora中随着JDK一起发布,并在Rhel7.4中作为技术预览. 通过-XX:+UseShenandoahGC运行标准的OpenJDK完全足够。

压力测试

关于CMS,G1,ParallelOld,Shenandoah的延迟测试对比,如下图所示:
image.png

请先登录,再评论

暂无回复,快来写下第一个回复吧~

为你推荐

不起眼,但是足以让你有收获的JVM内存分析案例
分析 这个问题说白了,就是说有些int[]对象不知道是哪里来的,于是我拿他的例子跑了跑,好像还真有这么回事。点该 dump 文件详情,查看相关的 int[] 数组,点该对象的“被引用对象”,发现所
协助美团kafka团队定位到的一个JVM Crash问题
概述 有挺长一段时间没写技术文章了,正好这两天美团kafka团队有位小伙伴加了我微信,然后咨询了一个JVM crash的问题,大家对crash的问题都比较无奈,因为没有现场,信息量不多,碰到这类问题我
又发现一个导致JVM物理内存消耗大的Bug(已提交Patch)
概述 最近我们公司在帮一个客户查一个JVM的问题(JDK1.8.0_191-b12),发现一个系统老是被OS Kill掉,是内存泄露导致的。在查的过程中,阴差阳错地发现了JVM另外的一个Bug。这个B
JVM实战:优化我的IDEA GC
IDEA是个好东西,可以说是地球上最好的Java开发工具,但是偶尔也会卡顿,仔细想想IDEA也是Java开发的,会不会和GC有关,于是就有了接下来对IDEA的GC进行调优 IDEA默认JVM参数: -
不起眼,但是足以让你收获的JVM内存案例
今天的这个案例我觉得应该会让你涨姿势吧,不管你对JVM有多熟悉,看到这篇文章,应该还是会有点小惊讶的,不过我觉得这个案例我分享出来,是想表达不管多么奇怪的现象请一定要追究下去,会让你慢慢变得强大起来,
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
谨防JDK8重复类定义造成的内存泄漏
概述 如今JDK8成了主流,大家都紧锣密鼓地进行着升级,享受着JDK8带来的各种便利,然而有时候升级并没有那么顺利?比如说今天要说的这个问题。我们都知道JDK8在内存模型上最大的改变是,放弃了Perm
JDK13 GA发布:5大特性解读
JDK13 GA版本 5大新特性如下: 350: Dynamic CDS Archives 351: ZGC: Uncommit Unused Memory 353: Reimplement the