前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JDK8使用G1 垃圾回收器能解决大问题吗?

JDK8使用G1 垃圾回收器能解决大问题吗?

作者头像
袁新栋-jeff.yuan
发布2022-05-05 14:50:27
1.2K0
发布2022-05-05 14:50:27
举报
文章被收录于专栏:用户4352451的专栏

本文想突出两个问题:

  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这就导致,我们服务不断出现超市请求。
    1. 继续从垃圾回收器的日志中得出垃圾回收的频率也高,大概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所以是需要设置参数指定的。
代码语言:javascript
复制
-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过程的一个必须环节。global concurrent marking的执行过程分为四个步骤:
    • 初始标记(initial mark,STW)。它标记了从GC Root开始直接可达的对象。
    • 并发标记(Concurrent Marking)。这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
    • 最终标记(Remark,STW)。标记那些在并发标记阶段发生变化的对象,将被回收。
    • 清除垃圾(Cleanup)。清除空Region(没有存活对象的),加入到free list
  6. Mixed 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数量。
  7. G1的常用参数配置: 参数含义-XX:G1HeapRegionSize=n设置Region大小,并非最终值-XX:MaxGCPauseMillis设置G1收集过程目标时间,默认值200ms,不是硬性条件-XX:G1NewSizePercent新生代最小值,默认值5%-XX:G1MaxNewSizePercent新生代最大值,默认值60%-XX:ParallelGCThreadsSTW期间,并行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.可预期的停顿时间

参考

  • https://www.cnblogs.com/aspirant/p/8663872.html
  • https://tech.meituan.com/2016/09/23/g1.html
  • https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-01-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 背景
  • 2. 问题分析
  • 3. 解决方案
  • 4. JDK8 升级G1
  • 5. G1学习和理解
    • 5.1 G1的几个重要概念
      • 5.2 G1的垃圾回收过程
      • 总结
      • 参考
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档