https://zhuanlan.zhihu.com/p/458098236

G1 垃圾回收器真的不行吗?

本文想突出两个问题:

  1. 解决问题的思路:从最原始的角度去思考,问题的本身是因为缓存数据导致的GC,那我们就应该去思考缓存数据是否合理,而不是去思考JVM的参数是否合理
  2. 学习G1的知识,其关键的概念,关键参数,已经相对CMS解决的两个问题:1.浮动垃圾 2.可预期的停顿时间

1. 背景

最近项目有两个问题

  1. 加了内存缓存,防止穿透到redis的missCache,导致大量的GC。
  2. 项目在每次发布的时候GC时间很长达到2s,导致大量的超时。

就针对这两个问题进行了分析和优化。

2. 问题分析

首先我们系统是内存32G,使用了大量的内存做为内存缓存数据。

结合业务和GC日志以及业务日志得出:

  1. 第一个启动的问题和第二个问题是一致的,都是因为内存缓存数据,导致每一次新来一个请求在redis查到数据后存入内存中去,导致存活对象在新生代不停的复制导致超时。
  2. 再者我们使用的是CMS垃回收器,新生代使用的是复制清除的垃圾回收机制,通过查看GC日志,每次存活的对象太多,以致于复制数据量很大。导致GC耗时和停顿时间较长,大概在200-300ms,又因为我们对外的借口客户端超时时间时200ms这就导致,我们服务不断出现超市请求。
  3. 继续从垃圾回收器的日志中得出垃圾回收的频率也高,大概13秒每次。

问题本质 :每次新生代存活对象太多

那既然已经看到问题的本质,那么我们应该怎么解决呢?

3. 解决方案

  1. 减小新生代的大小,让每次复制的对象小一点,但是会引发另一个问题GC的频率会提高,虽然停顿时间短了但是停顿的频率会飙高。
  2. 调整进入老年代的年龄,默认的15次,调整为6-7次。让提前进入老年代。
  3. 更换新的垃圾回收器,使用G1
  4. 优化业务逻辑,调整内存缓存key的时间。

这四个方案是有先后顺序的,这些方案提出顺序也意味着我当时在执行实验的顺序,可以看出这是一个糟糕的方案顺序,不记得谁说的了应该是鲁迅吧:“调参JVM是迫不得已的选择!”。

在实验过程中细节比较多就不细讲了,就着重讲一下升级为G1的方案,因为我这篇文章的主要目的是学习G1。

4. JDK8 升级G1

  1. G1在jdk6的时候是已经出现了,JDK 7 u9 或更高版本可以使用,在jdk9的时候成为默认的垃圾回收器。因为我们是jdk8所以是需要设置参数指定的。
-Xms24g -Xmx24g -XX:+UseG1GC -XX:MaxGCPauseMillis=95
//最大堆内存24G 使用G1GC,设置预期停顿时间是95ms
  1. 使用G1的主要原因是:G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。
    目标很明确,可控的GC时间。
  2. 升级启用了G1后解惑不尽人意,甚至比CMS的结果还差,我们来看下G1,几个优于CMS的几个特点,以及实现。

5. G1学习和理解

5.1 G1的几个重要概念

  1. Region
  • 传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址(逻辑地址,下同)是连续的。如下图所示

  • 对于G1的各代存储地址是不连续的,每一代都使用了n个不连续大小的region,每个Region占有一块连续的虚拟内存地址

  • 如上图,region区分为四种分别是Eden,sourivor,Old, 我们注意到还有一些Region标明了H,它代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。H-obj有如下几个特征: H-obj直接分配到了old gen,防止了反复拷贝移动。 *H-obj在global concurrent marking阶段的cleanup 和 full GC阶段回收。 在分配H-obj之前先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。
  1. SATB
  • 全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。 那么它是怎么维持并发GC的正确性的呢?
  • 根据三色标记算法,我们知道对象存在三种状态:
  1. 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
  2. 灰:对象被标记了,但是它的field还没有被标记或标记完。 *
  3. 黑:对象被标记了,且它的所有field也被标记完了。
  • 对于第一个条件,在并发标记阶段,如果该白对象是new出来的,并没有被灰对象持有,那么它会不会被漏标呢?Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。对于在GC时已经存在的白对象,如果它是活着的,它必然会被另一个对象引用,即条件二中的灰对象。如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误,所以SATB破坏了第二个条件。也就是说,一个对象的引用被替换时,可以通过write barrier 将旧引用记录下来。
  • SATB也是有副作用的,如果被替换的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,这就是float garbage。因为SATB的做法精度比较低,所以造成的float garbage也会比较多。
  1. RSet(跨region的对象引用关系的处理)
  • 全称是Remembered Set,是辅助GC过程的一种结构,典型的空间换时间工具,和Card Table有些类似。
  • 还有一种数据结构也是辅助GC的:Collection Set(CSet),它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。
  • 在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。 逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。 这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index.

    • 上图中有三个Region,每个Region被分成了多个Card,在不同Region中的Card会相互引用,Region1中的Card中的对象引用了Region2中的Card中的对象,蓝色实线表示的就是points-out的关系,而在Region2的RSet中,记录了Region1的Card,即红色虚线表示的关系,这就是points-into。 而维系RSet中的引用关系靠post-write barrier和Concurrent refinement threads来维护.类似于AOP切面的过程,在进行引用更换操作的时候需要进行修改。
    • post-write barrier记录了跨Region的引用更新,更新日志缓冲区则记录了那些包含更新引用的Cards。一旦缓冲区满了,Post-write barrier就停止服务了,会由Concurrent refinement threads处理这些缓冲区日志。
    • RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。
  1. Pause Prediction Model
  • Pause Prediction Model 即停顿预测模型。它在G1中的作用是: >G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.
  • G1 GC是一个响应时间优先的GC算法,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间,参数-XX:MaxGCPauseMillis指定一个G1收集过程目标停顿时间,默认值200ms,不过它不是硬性条件,只是期望值。那么G1怎么满足用户的期望呢?就需要这个停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。
  • 我们呢可以知道,根据设置停顿时间来决定回收内存的大小。

了解了这几个概念后我们来看,G1清理垃圾的过程是怎么进行的呢?
5.2 G1的垃圾回收过程

    1. G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。
    2. Young GC:选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
    3. Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。(用户是上帝的原则)
    4. 由上面的描述可知,Mixed GC不是full GC(回收全量的老年代),它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。所以我们可以知道,G1是不提供full GC的。
    5. global concurrent marking,它的执行过程类似CMS,但是不同的是,在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。
    6. global concurrent marking的执行过程分为四个步骤:
  • 初始标记(initial mark,STW)。它标记了从GC Root开始直接可达的对象。
  • 并发标记(Concurrent Marking)。这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
  • 最终标记(Remark,STW)。标记那些在并发标记阶段发生变化的对象,将被回收。
  • 清除垃圾(Cleanup)。清除空Region(没有存活对象的),加入到free listMixed GC发生的时机:
  • G1HeapWastePercent :在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数G1HeapWastePercent,只有达到了,下次才会发生Mixed GC。
  • G1MixedGCLiveThresholdPercent:old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet。
  • G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数。
  • G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多old generation region数量。

G1的常用参数配置:

7. G1的常用参数配置(垃圾编辑器表格都复制不了):

| 参数 | 含义 |

| :--------------------------------- | :----------------------------------------------------------- |

| -XX:G1HeapRegionSize=n | 设置Region大小,并非最终值 |

| -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 |

总结

  • 写了这么多,从遇到GC问题到问题解决(最后是通过最简单的方式,那就是内存缓存时间的设置合理),最后是使用最方便和最简单的方式解决的。我们一直是为了解决问题而解决最表面的问题。我们深知是因为是内存缓存而导致的问题,而没有从内存缓存的合理性上去思考,而是花费大量的时间和精力的去调优JVM。突然在这里想到musk讲的第一性原理。从最本质去解决问题。
  • 本文想突出两个问题:
  1. 解决问题的思路:从最原始的角度去思考,问题的本身是因为缓存数据导致的GC,那我们就应该去思考缓存数据是否合理,而不是去思考JVM的参数是否合理
  2. 学习G1的知识,其关键的概念,关键参数,已经相对CMS解决的两个问题:1.浮动垃圾 2.可预期的停顿时间

参考

[转帖]JDK8使用G1 垃圾回收器能解决大问题吗?的更多相关文章

  1. G1垃圾回收器

    垃圾回收器的发展历程 背景 01.G1解决的问题 G1垃圾回收器是04年正式提出,12开始正式支持,在17年作为JDK9默认的垃圾处理器. 在04年的时候,java程序堆的内存越来越大,从而导致程序中 ...

  2. 深入浅出具有划时代意义的G1垃圾回收器

    G1诞生的背景 Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式.HotSpot开发团队最初 ...

  3. JVM学习——G1垃圾回收器(学习过程)

    JVM学习--G1垃圾回收器 把这个跨时代的垃圾回收器的笔记独立出来. 新生代:适用复制算法 老年代:适用标记清除.标记整理算法 二娃本来看G1的时候觉得比较枯燥,但是后来总结完之后告诉我说,一定要慢 ...

  4. G1垃圾回收器在并发场景调优

    一.序言 目前企业级主流使用的Java版本是8,垃圾回收器支持手动修改为G1,G1垃圾回收器是Java 11的默认设置,因此G1垃圾回收器可以用很长时间,现阶段垃圾回收器优化意味着针对G1垃圾回收器优 ...

  5. G1 垃圾回收器简单调优

    G1: Garbage First 低延迟.服务侧分代垃圾回收器. 详细介绍参见:JVM之G1收集器,这里不再赘述. 关于调优目标:延迟.吞吐量 一.延迟,单次的延迟 单次的延迟关系到服务的响应时延, ...

  6. 探索G1垃圾回收器

    前言 最近王子因为个人原因有些忙碌,导致文章更新比较慢,希望大家理解,之后也会持续和小伙伴们一起共同分享技术干货. 上篇JVM的文章中我们对ParNew和CMS垃圾回收器已经有了一个比较透彻的认识,感 ...

  7. JAVA之G1垃圾回收器

    概述 G1 GC,全称Garbage-First Garbage Collector,通过-XX:+UseG1GC参数来启用,作为体验版随着JDK 6u14版本面世,在JDK 7u4版本发行时被正式推 ...

  8. jvm默认的并行垃圾回收器和G1垃圾回收器性能对比

    http://www.importnew.com/13827.html 参数如下: JAVA_OPTS="-server -Xms1024m -Xmx1024m -Xss256k -XX:M ...

  9. G1垃圾回收器参数配置

    下面是完整的 G1 的 GC 开关参数列表. 选项/默认值 说明 -XX:+UseG1GC 使用 G1 (Garbage First) 垃圾收集器 -XX:MaxGCPauseMillis=n 设置最 ...

  10. JVM七大垃圾回收器下篇G1(Garbage First)

    G1回收器:区域化分代式 既然我们已经有了前面几个强大的GC,为什么还要发布Garbage First (G1)GC?  原因就在于应用程序所应对的业务越来越庞大.复杂,用户越来越多,没有GC就不能保 ...

随机推荐

  1. SQL注入上传文件获取shell

    SQL注入写文件的三个必要条件 Web目录有读写权限: 当目标网站的Web目录具有读写权限时,攻击者可以通过注入恶意SQL语句将恶意文件写入服务器上的Web目录. 知道文件的绝对路径: 攻击者需要知道 ...

  2. CSS3学习笔记-字体属性

    在CSS3中,可以使用字体属性来控制网页中文本的样式和排版.以下是常用的字体属性: font-family 该属性用于指定网页中的文本所使用的字体.我们可以通过使用通用的字体名称,或者直接使用字体名称 ...

  3. 【K8S系列】如何高效查看 k8s日志

    序言 你只管努力,其他交给时间,时间会证明一切. 文章标记颜色说明: 黄色:重要标题 红色:用来标记结论 绿色:用来标记一级论点 蓝色:用来标记二级论点 Kubernetes (k8s) 是一个容器编 ...

  4. 谈谈muduo库的销毁连接对象——C++程序内存管理和线程安全的极致体现

    前言 网络编程的连接断开一向比连接建立复杂的多,这一点在陈硕写的muduo库中体现的淋漓尽致,同时也充分体现了C++程序在对象生命周期管理上的复杂性,稍有不慎,满盘皆输. 为了纪念自己啃下muduo库 ...

  5. 获取yml自定义内容的方式

    yml内容 yml: login: name: zhangsan age: 18 pass: 123456 方式一: 创建实体类 @Configuration @ConfigurationProper ...

  6. 手把手教你在昇腾平台上搭建PyTorch训练环境

    摘要:在昇腾平台上运行PyTorch业务时,需要搭建异构计算架构CANN软件开发环境,并安装PyTorch 框架,从而实现训练脚本的迁移.开发和调试. 本文分享自华为云社区<手把手教你在昇腾平台 ...

  7. 这项评测,华为云GaussDB(for MySQL)顺利通过

    摘要:近日,中国信息通信研究院(简称"中国信通院")公布了第十五批"可信数据库"评测结果.华为云GaussDB(for MySQL)凭借过硬的技术实力顺利通过& ...

  8. YoloV5实战:手把手教物体检测

    摘要:​YOLOv5并不是一个单独的模型,而是一个模型家族,包括了YOLOv5s.YOLOv5m.YOLO... 本文分享自华为云社区<YoloV5实战:手把手教物体检测--YoloV5> ...

  9. 火山引擎 DataTester:抖音的设计团队是如何用 A/B 测试实现高效优化的?

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 对 C 端产品而言,产品的每一个细节设置都或多或少影响着用户的产品体验,本文介绍字节跳动的 A/B 实验文化的同时 ...

  10. Linux--内存管理浅谈

    本文旨在宏观的了解linux的内存管理制度,理解相关概念,故很多方面不作过深叙述,感兴趣的可自行研究:如有错误,请指出. 一.进程与内存 当一个进程启动时,它需要获取系统分配给它的内存空间,并且设置好 ...