G1 垃圾回收器的工作流程

2021-01-30 14:33:32   最后更新: 2021-01-30 14:33:32   访问数量:426




 

上一篇文章中,我们介绍了 CMS 垃圾回收机制的具体回收过程:

 

CMS 执行的七个阶段

 

我们看到,CMS 的垃圾回收机制下,想要做到性能的调优,超强的耐心与丰富的经验是必不可少的,因为整个回收过程相关的 jvm 参数就有几十个之多,如何才能将 CMS 回收机制调整到最适合当前场景的使用是困扰诸多 java 程序员的一大问题。

 

出于对上述问题的优化,G1 垃圾回收器诞生了,他旨在让开发人员通过简单的参数实现系统性能的调优:

 

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

 

  • -XX:+UseG1GC 是必须的,它用来告诉 jvm 开启 G1 垃圾收集器
  • -Xmx32g 设置了最大堆内存的大小
  • -XX:MaxGCPauseMillis 用来设置最大停顿时间

 

这样配置之后,你就可以使用 G1 垃圾收集器了,相比于 CMS 是不是很轻松呢?

 

由于 G1 拥有垃圾回收时间的预测机制,因此他可以保证在你设置的最大停顿时间内完成垃圾回收,这可以说是 G1 垃圾回收器最令人惊喜的特性了。

 

当然,除了上述三个参数,还有几个可选的参数供开发人员配置,但相比于 CMS,仍然无疑是更加容易进行调优的。

 

那么,G1 垃圾收集器是如何实现的呢?相比于 CMS 他又有哪些优势和不足呢?本文就来详细介绍一下。

 

 

G1 垃圾回收器的第一篇 paper 在 2004 年发表,到 2012 年终于被加入到 jdk 1.7u4 中,到了 jdk9,G1 已经变成了默认的垃圾收集器,可以参看官方文档:

 

https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector.htm#JSGCT-GUID-ED3AB6D3-FD9B-4447-9EDF-983ED2F7A573

 

与传统的分代垃圾回收器不同,G1 不再将堆内存分为预设的年轻代与老年代,而是将堆内存划分为若干不连续但大小相同的区域 -- Region,每个 Region 占据一块连续的虚拟内存地址空间。

 

如图所示,Region 有四种类型:

 

  1. Eden 区 -- 新生代
  2. Survive 区 -- 幸存区
  3. Old 区 -- 老年代
  4. Humongous 区 -- 巨大对象存储区

 

新生代、幸存区、老年代的定义与 CMS 中并没有明显区别,但 G1 中新增了 Humongous 存储区域,这部分空间用来存储超过 Region 大小一半的超大对象。

 

默认情况下,G1 会根据堆内存的大小自动设定每个 Region 的大小,我们也可以通过下面的参数设定一个 Region 的大小,取值范围是 1M 到 32M,且必须是 2 的指数:

 

-XX:G1HeapRegionSize=2M

 

 

 

 

既然我们知道了 G1 管理下的内存分区,那么,如果要新创建一个对象,他会被分配到哪个内存区域呢?

 

G1 对于对象分配有三个级别:

 

  1. TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
  2. 在 Eden 区进行分配
  3. 在 Humongous 区进行分配

 

在 Eden 空间中,G1 为每个分配线程分配了一段独立的 ThreadLocal 空间,这样可以有效避免并发过程中的同步问题,从而保证让对象分配更快的进行。

 

如果 TLAB 内的空间无法完成分配,那么就会尝试在 Eden 空间中进行分配,也就是尝试在某个 Region 内进行分配。

 

如果对象的大小超过了 Region 一半的大小,那么,就只能在 Humongous 空间中进行分配了。

 

 

与此前其他垃圾回收器一样,G1 回收器也分为对 Eden 区进行的 Young GC 以及对老年代进行 的 Mix GC。

 

4.1 G1 中的 RSet 与 Card Table

 

对于只针对新生代或老年代进行的 GC,很显然,不能只扫描对应的分区,因为可能会有老年代对象引用新生代对象或新生代对象引用老年代的对象的情况,这样的话,无论回收哪个分区,都必须要对整个堆内存进行扫描,这显然是过于耗时而不能接受的。

 

正如我们在介绍 CMS 流程时所介绍的,CMS 通过 RSet 和 Card Table 实现了对上述情况的标记,G1 也使用了类似策略。

 

RSet 就是 RememberedSet,他跟踪了跨越 heap 区的引用,与 CMS 相反,G1 没有记录当前分区引用了哪些分区,而是在 RSet 中记录了哪些分区引用了当前分区的对象,这是因为 G1 采用细分的 Region 造成分区过多,同时,一个对象可能会引用非常多个对象,如果记录当前分区引用的分区,扫描范围仍然会很大。

 

但是,光是记录哪些分区引用了当前分区,并且去扫描这些引用了当前分区的分区中的对象,扫描的开销仍然很大,因此 G1 引入了 Card Table,和 CMS 中一样,一个 Card Table 将一个 Region 划分为 128 到 512 字节之间为单位的若干个 Card,通过标记 Card 是否为脏及是否被引用,可以让上述扫描过程进一步精细化。

 

 

 

4.2 SATB

 

G1 的回收过程整体上分为标记过程与回收过程,由于标记过程是并发进行的,和 CMS 一样,会在标记过程中产生新的漏标对象,从而可能造成对象被错误地回收掉。

 

为了避免这种情况的发生,G1 引入了 SATB,他是 Snapshot-At-The-Beginning 的缩写。

 

SATB 机制维护了一个双向链表,指向每个新分配的对象,这样垃圾回收器可以轻松的知道在开始 GC 后新分配的对象,从而避免对象被误回收,但这样会造成回收过程中产生的新的垃圾对象没有被回收,造成 float garbage。

 

4.3 Young GC

 

基于上述的数据结构,Young GC 的主要工作流程是:

 

  1. 标记 -- 扫描静态和本地对象
  2. 处理 dirty card,更新 RSet
  3. 检测从年轻代指向老年代的对象
  4. 对象拷贝
  5. 处理软引用、弱引用与虚引用

 

事实上,回收的重点在于对象拷贝的过程,在这一过程中,G1 将 Eden 区的活跃对象拷贝到 survive 区,如果 survive 区满,则会直接晋升到 old 区,survive 区中经过多次没有回收的对象也会晋升到 old 区。

 

 

 

4.4 Mix GC

 

Mix GC 是用来对老年代进行回收的,他分为两步:

 

  1. 全局并发标记
  2. 拷贝存活对象

 

全局并发标记共分为五个阶段:

 

  1. 初始标记 -- 对 GC Roots 进行标记,需要 Stop The World
  2. 根区域扫描 -- 在初始标记的存活 Region 中扫描对老年代的引用,并标记被引用对象
  3. 并发标记 -- 在整个堆中标记存活对象,可中断
  4. 最终标记 -- 清空 SATB 缓冲区,跟踪未被访问的存活对象,该阶段需要 Stop The World
  5. 清除垃圾 -- 也就是并发清除阶段,在执行 GC 统计和净化 RSet 过程中需要 Stop The World

 

4.5 Full GC

 

由于划分 Region,让 G1 更不容易产生内存碎片,但无论如何,内存碎片还是会随着 jvm 的工作而不断产生。

 

在 Mix GC 之前,老年代已经被填满,或者 Young GC 过程中向老年代晋升的内存分配失败,或者分配巨型对象失败,都会触发串行化的 Full GC

 

 

G1 最大的优势在于将堆内存空间离散化,从而提升对象分配和回收的效率,减少内存碎片的产生。

 

与此同时,G1 垃圾回收器还拥有停顿预测模型,从而能够尽量满足用户所设定的 GC 预期停顿时间,从而让整个回收过程更加可控。

 

但是,G1 的停顿预测功能并不能将回收过程精准的保证在所设定的时间范围内。

 

 

上面介绍了 G1 使用的基本参数,可以看到 G1 垃圾收集器并不需要用户配置繁琐的参数就可以较好的工作,但我们仍然可以通过有限的参数来进行性能调优。

 

G1 拥有以下参数:

 

参数 含义
-XX:G1HeapRegionSize=n 设置Region大小,并非最终值,因为最终只能取 2 的幂大小
-XX:MaxGCPauseMillis 设置G1收集过程目标时间,默认值200ms,不是硬性条件
-XX:G1NewSizePercent 新生代最小值,默认值5%
-XX:G1MaxNewSizePercent 新生代最大值,默认值60%
-XX:ParallelGCThreads STW期间,并行GC线程数
-XX:ConcGCThreads=n 并发标记阶段,并行执行的线程数
-XX:InitiatingHeapOccupancyPercent 设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包括old+humongous

如果大对象比较多,可以将 Region 适当设置大一些,从而防止频繁分配巨型对象。

 

ParallelGCThreads 参数通常不建议设置 8 以上,除非处理器不止 8 个,建议设置为处理器的 5/8 左右,ConcGCThreads 则建议设置为 ParallelGCThreads  的 1/4,防止过多占用工作线程的资源。

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,全部原创,只有干货没有鸡汤

 

 

 

 

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html

 

http://dl.acm.org/citation.cfm?id=1029879

 

https://hllvm-group.iteye.com/group/topic/44381

 

https://hllvm-group.iteye.com/group/topic/44529

 

https://tech.meituan.com/2016/09/23/g1.html

 

http://ghoulich.xninja.org/2018/01/27/understanding-g1-garbage-collector-in-java/

 

 

java 专题






java      jvm      gc      垃圾回收      g1      垃圾回收器      hospot     


京ICP备2021035038号