专栏首页微信公众号:Java团长为什么G1 GC从JDK 9之后成为默认的垃圾回收器?

为什么G1 GC从JDK 9之后成为默认的垃圾回收器?

从JDK 9开始G1替代并行垃圾回收器成为JVM中默认的垃圾回收器(具体可见JEP提案248,地址为https://openjdk.java.net/jeps/248),并且官方将CMS标记为丢弃(具体可见JEP提案291,地址为https://openjdk.java.net/jeps/291)。G1能够脱颖而出,成为最大的赢家,其最主要的原因就是在过去几年间,众多使用者使用G1之后发现G1的性能表现的非常优秀。

目前JVM提供的正式垃圾回收器有并行、串行、CMS和G1。这四种垃圾回收器都是基于分代内存管理。分代管理就是把内存划分成不同的区域进行管理,其思想来源是:有些对象存活的时间短,有些对象存活的时间长,把存活时间短的对象放在一个区域管理,把存活时间长的对象放在另一个区域管理,那么可以为两个不同的区域选择不同的算法,加快垃圾回收的效率。我们假定内存被划分成2个代,新生代和老生代。把容易死亡的对象放在新生代,通常采用复制算法回收,把预期存活时间较长的对象放在老生代,通常采用标记清除算法。

虽然并行、串行、CMS和G1都是采用了分代的内存管理,垃圾回收时采用的算法也都是复制算法或者标记清除算法。但是每一种垃圾回收器的实现并不相同。其主要的区别可以总结为:

  • 内存管理方式不同。
  • 回收算法实现不同。

内存管理

在并行、串行以及CMS中针对堆空间的管理方式都是连续的。如下图所示:

连续的内存将导致垃圾回收时收集时间过长,停顿时间不可控。在某些场景中因垃圾回收导致应用程序暂停超过数分钟、数秒等屡见不鲜。

所以G1将堆拆成一系列的分区(Heap Region),这样在一个时间段内,大部分的垃圾回收操作就只是针对一部分分区执行,而不是整个堆或整个(老年)代,从而满足在指定的停顿时间内完成垃圾回收的动作。G1内存分区如下图所示:

在G1新生代就是一系列的内存分区,这意味着不用再要求新生代是一个连续的内存块。类似地,老生代也是由一系列的分区组成。在JVM运行时,从内存管理角度不需要预先设置分区是老生代分区还是新生代分区,而是在内存分配时决定,当新生代需要空间则分区被加入到新生代中,当老生代需要内存空间则分区被加入到老生代中。事实上,G1通常的运行状态是:映射G1分区的虚拟内存随着时间的推移在不同的代之间切换。例如一个G1分区最初被指定为新生代,经过一次新生代的回收之后,整个新生代分区都被划入到待使用的分区中,那它就可以作为新生代分区使用,也可以作为老生代分区使用。很可能在完成一个新生代回收之后,一个新生代的分区在未来的某个时刻被用于老生代分区。同样地,在一个老生代分区完成回收之后,它就成为待使用分区,在未来某个时候作为一个新生代分区来使用。

G1新生代的回收方式是并行回收,采用复制算法。与其他JVM垃圾回收器一样,一旦发生一次新生代回收,整个新生代都会被回收。这也就是我们常说的新生代回收(Young GC,简称为YGC)。但是G1和其他垃圾回收器不同的地方在于:一、G1会根据预测时间动态的改变新生代的大小(G1中预测时间是根据运行垃圾回收的历史数据通过数学建模预测得到,所用的数学模型是衰减平均);二、G1老生代的垃圾回收方式与其他JVM垃圾回收器对老生代处理有着极大的不同。G1老生代的回收不会为了释放老生代的空间对整个老生代做回收。相反,在任意时刻只有一部分老生代分区会被回收,并且,这部分老生代分区将在下一次增量回收时与所有的新生代分区一起被回收。这就是我们所说的混合回收(Mixed GC),在选择老生代分区的时候,优先考虑垃圾多的分区。

回收算法

串行垃圾回收器

使用单线程进行垃圾回收,在回收的时候应用程序需要暂停执行。新生代通常采用复制算法,老生代通常采用标记压缩算法。串行回收典型的垃圾回收活动图如下所示:

并行回收回收器

使用多线程进行垃圾回收,在回收的时候应用程序需要暂停,新生代通常采用复制算法,老生代通常采用标记压缩算法。垃圾回收活动图如下:

CMS

整个回收期间划分成多个阶段:初始标记、并发标记、重新标记、并发清除等阶段。在初始标记和重新标记阶段需要暂停应用程序,在并发标记和并发清除期间可以和应用程序并发运行。这个算法通常适用于老生代,新生代可以采用并行复制回收,也可以采用串行复制算法。垃圾回收活动图如下:

同样在老生代回收时,因为是并发执行,当在内存分配时发现如果内存不足需要进行Full GC,也需要STW对整个内存进行串行回收。

G1

G1新生代的收集方式是并行收集,采用复制算法。与其他JVM垃圾回收器一样,一旦发生一次新生代回收,整个新生代都会被回收。这也就是我们常说的新生代回收(Young GC,简称为YGC)。但是G1和其他垃圾回收器不同的地方在于:一、G1会根据预测时间动态的改变新生代的大小;二、G1老生代的垃圾回收方式与其他JVM垃圾回收器对老生代处理有着极大的不同。G1老生代的收集不会为了释放老生代的空间对整个老生代做回收。相反,在任意时刻只有一部分老生代分区会被回收,并且,这部分老生代分区将在下一次增量回收时与所有的新生代分区一起被收集。这就是我们所说的混合回收(Mixed GC),在选择老生代分区的时候,优先考虑垃圾多的分区。

老生代分区的选择就涉及到G1的并发标记算法,这个过程称为“并发标记阶段”,并发标记是是指并发标记线程和应用程序线程同时运行,它有4个典型的子阶段:初始标记子阶段、并发标记子阶段、再标记子阶段和清理子阶段。

1) 初始标记子阶段

负责标记所有能被直接可达的根对象(栈对象、全局对象、JNI对象等),根是对象图的起点,因此初始标记需要将Mutator线程暂停掉,也就是需要一个STW的时间段。在混合回收中的初始标记和新生代的初始标记几乎一样。实际上混合回收的初始标记是借用了新生代回收的结果,即新生代垃圾回收后的新生代Survivor分区作为根,所以混合回收一定发生在新生代回收之后,且不需要再进行一次初始标记。这就是所谓的“借道”。

2) 并发标记子阶段

当YGC执行结束之后,如果发现满足并发标记的条件之后,并发线程就开始进行并发标记。根据新生代的Survivor分区以及老生代的RSet(RSet是记录对象的引用关系,目的是加快活跃对象的遍历)开始并发标记。并发标记的时机是在YGC后,只有内存消耗达到一定的阈值后,才会触发。在G1中这个阈值通过参数InitiatingHeapOccupancyPercent控制(默认值是45,表示的是当已经分配的内存加上即将分配的内存超过内存总容量的45%就可以开始并发标记)。多个并发标记线程启动,每个线程每次只扫描一个分区,从而标记出存活对象。在标记的时候还会计算存活的数量,同时会被计算字节数,并计入分区空间。

并发标记子阶段会对所有的分区的对象进行标记。这个阶段并不需要STW,故标记线程和应用程序线程并发运行。使用Snapshot-At-The-Begging(简称为SATB)算法进行并发标记。

3) 再标记子阶段

再标记是最后一个标记阶段。在该阶段中,G1需要一个STW的时间段,找出所有未被访问的存活对象,同时完成存活内存数据计算。引入该阶段的目的,是为了能够达到结束标记的目标。

4) 清理子阶段

再标记子阶段之后是清理子阶段,该子阶段也是需要一个STW的时间段。清理子阶段主要执行以下操作:

  • 统计存活对象,统计的结果将会用来排序分区,以用于下一次的垃圾回收时分区的选择;
  • 交换标记位图,为下次并发标记准备;
  • 把空闲分区放到空闲分区列表中;这里的空闲指的是全都是垃圾对象的分区;如果分区还有任何活跃对象都不会释放,真正释放是在Mixed GC中;

在并发标记阶段完成之后,在下一次进行垃圾回收的时候就会把垃圾比较多的老生代分区进行回收。这时进行垃圾回收称为混合回收,混合回收和YGC最大的区别就是不仅仅回收所有的新生代分区,也回收部分垃圾多的老生代分区,所以JVM在实现时混合回收重用了YGC所有的代码,两者不同就是是否回收老生代分区。整个G1 GC的活动图如下所示:

注意,在上图并发标记阶段中还可以发生YGC(可以是一次YGC,也可以是多次YGC);另外在图中混合回收也可能发生多次,因为G1对停顿时间有要求的,G1会预测停顿时间决定一次回收老生代分区的数目,所以可能需要多次混合回收才能完成并发标记阶段识别的垃圾比较多的老生代分区。

最后同样在垃圾回收过程或者并发执行过程中,如果内存不足需要进行Full GC时,也需要STW对整个内存进行串行回收。在Java 10对Full GC做了改进,把串行回收改进成并行回收,注意是并行的Full GC,而不是并发回收。

(完)

本文摘编自《JVM G1源码分析和调优》,经出版方授权发布。

适读人群:全部Java工程师

详细分析G1的基本运行原理以及调优方法,讲解细腻,图示丰富,可帮助Java工程师深入理解垃圾回收技术。

  • 图文并茂——作者亲手绘制了大量图片,使读者能直观看懂G1的底层原理。
  • 深入浅出——分步骤解析新生代回收、混合回收、Full GC等原理,还绘制了典型过程的流程图。
  • 讲解细腻——对典型代码进行了大量注释,使读者更深入了解垃圾回收的实现原理。
  • 实操性强——不仅列出了G1的各种参数,而且给出了实际操作中的各种权衡思路。

本文分享自微信公众号 - Java团长(javatuanzhang)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-05-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 这一次,让你彻底理解Java的值传递和引用传递!

    学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而且坊间讨论的...

    Java团长
  • 什么是JVM?

    说明:做java开发的几乎都知道jvm这个名词,但是由于jvm对实际的简单开发的来说关联的还是不多,一般工作个一两年(当然不包括爱学习的及专门做性能优化的什么的...

    Java团长
  • Java编程的21个常见错误

    代码重复几乎是最常见的异味了。他也是Refactoring 的主要目标之一。代码重复往往来自于copy-and-paste 的编程风格。与他相对应OAOO 是...

    Java团长
  • Opportunity的chance of success的赋值逻辑

    该字段的值和另外两个字段Sales Stage和Status都相关。

    Jerry Wang
  • 【python】读取json文件

    最近要打个比赛,在处理数据的时候,发现数据竟然是json文件的,于是上网查了下,展示给大家O.O

    zenRRan
  • Java过滤器Filter的使用详解

    过滤器 过滤器是处于客户端与服务器资源文件之间的一道过滤网,在访问资源文件之前,通过一系列的过滤器对请求进行修改、判断等,把不符合规则的请求在中途拦截或修改。也...

    nnngu
  • Impala元数据缓存的生命周期

    上一篇文章《Impala元数据简介》介绍了Impala缓存的元数据(Metadata/Catalog)的具体内容,本文将介绍这些元数据缓存的生命周期,即它们是怎...

    Fayson
  • Express开发实战

    今天为了制作compass-style.org国内网站,决定使用nodejs来开发,express作为nodeJs 快速开发框架成为不二选择。半年前就学过nod...

    用户1065635
  • VueJS 常用系统指令

    可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码

    cwl_java
  • 如何通过命令行 msbuild 编译项目

    本文告诉大家如何通过 msbuild 编译一个项目,通过命令行编译可以输出更多的编译信息,可以用来调试自己写的编译相关方法,可以看到是哪个文件编译失败

    林德熙

扫码关注云+社区

领取腾讯云代金券