前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >驾驭一切的垃圾收集器 -- G1

驾驭一切的垃圾收集器 -- G1

作者头像
用户3147702
发布2022-06-27 15:59:23
3810
发布2022-06-27 15:59:23
举报

1. 引言

上一篇文章中,我们介绍了 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 他又有哪些优势和不足呢?本文就来详细介绍一下。

2. G1 与内存分区

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

3. 对象分配策略

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

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

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

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

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

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

4. G1 回收过程

与此前其他垃圾回收器一样,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

5. G1 的优势和不足

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

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

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

6. 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/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. G1 与内存分区
  • 3. 对象分配策略
  • 4. G1 回收过程
    • 4.1 G1 中的 RSet 与 Card Table
      • 4.2 SATB
        • 4.3 Young GC
          • 4.4 Mix GC
            • 4.5 Full GC
            • 5. G1 的优势和不足
            • 6. G1 的相关参数
            • 附录 - 参考资料
            相关产品与服务
            对象存储
            对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档