记录一次G1垃圾收集器调优经历

个人理解与建议且概念简述不全,如有不周还请谅解

一、G1中的重要概念简述

1.G1原理简述

G1是Garbage-First的缩写,与CMS相比,G1是一种有内存整理过程的、优先收集存活对象最少区域的、面向年轻代和老年代的、物理空间不必连续的垃圾收集器。

Region:整个堆空间被尽可能地划分为约2048个Region(单个Region大小为1~32MB),每个Region都可以是Eden/Survivor/Old区的任意一个Remembered Sets(RSets):每个Region对应一个RSet,用来跟踪指向该区域的对象引用,避免全堆扫描Collection Sets(CSets):CSets是一次GC中将被回收的Region集合,GC时所有CSet区域中的存活对象都会被移动到新的区域Humongous Object:超过Region大小一半的对象称为巨型对象,会在创建时直接被分配到Old Regions中

2.工作模式

G1是同时面向年轻代和老年代的垃圾收集器,因此包括Young GC 和 Mixed GC两种工作模式。Young GC:

  • Young 区由一系列不连续的Region组成,因此可以很容易在GC过程中扩/缩容
  • Young GC 会将存活对象转移到Survivor和老年代
  • Young GC 会计算 Eden 区和 Survivor 区在下次回收之前所需的的空间,然后动态调整新生代Region个数,从而达到控制Young GC耗时的目的

Mixed GC:

  • 当老年代使用的空间超过阈值(IHOP),会触发 Global Concurrent Marking 统计出回收收益高的老年代Region,然后开始一系列连续的Mixed GC,这个过程称为Concurrent Marking Cycle Phases
  • 一系列连续的Mixed GC中,GC的最大次数和单次回收的空间大小可以通过参数-XX:G1MixedGCCountTarget和-XX:G1OldCSetRegionThresholdPercent进行配置
  • 除了GC次数超过最大值以外,如果可回收空间的比例小于参数 -XX:G1HeapWastePercent ,也会中断Mixed GC周期
  • Mixed GC 是面向全堆进行垃圾收集的,所以通常会伴随一次Young GC,但与Full GC不同的是,Mixed GC只能回收部分老年代的Region
  • Mixed GC过程中会将多个老年代Region中仍存活的小对象集中到一个Region中,也就是说Mixed GC会对老年代空间进行内存整理
  • G1 GC 提供了一种停顿时间预测模型,用户可以通过参数-XX:MaxGCPauseMillis设定每次GC的期望停顿时间

3.Concurrent Marking Cycle Phases:

Concurrent Marking Cycle Phases包括Global Concurrent Marking和Mixed GCs,具体步骤如下图,需要注意的是Global Concurrent Marking不是GC过程,并不会和Mixed GC同时发生,它只负责标记、统计出收集收益高的老年代region

Initial-mark:标记所有能直接可达的根对象,当达到IHOP阈值时并不会直接触发,而是等待下一次Young GC,利用其STW时间完成初始标记

Concurrent Marking:和应用程序线程并发的执行,从GC Roots开始对堆中的对象标记,标记出各个region中存活的对象

Remark:暂停所有应用线程,重新扫描堆中的对象,标记在并发标记阶段发生变化的对象;

Clean:统计有存活对象和没有存活对象的region,更新RSet,把完全空闲的region收集到可分配队列Evacuation Reclaim:拷贝整理老年代存活对象的过程,需要暂停应用程序

重点讲一下混合回收

这个阶段会计算老年代中每个Region中存活对象的数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。接着会STW,进行垃圾回收,并将回收时间控制在我们设定的预期停顿时间之内。

混合回收回收的不仅是老年代,还有新生代和大对象。G1会从新生代,老年代和大对象中选择一部分的Region,尽可能多的回收垃圾对象。G1中有一个参数 -XX:G1MixedGCCountTarget,默认为8,这个参数标识最后的混合回收阶段会执行8次,一次只回收掉一部分的Region,然后系统继续运行,过了一小段时间之后,又再次进行混合回收,重复8次。执行这种间断的混合回收,就可以把每次的混合回收时间控制在我们需要的停顿时间之内了,同时达到垃圾清理的效果。

还有一个参数 -XX:G1HeapWastePercent,默认为5,它的意思是在混合回收的时候,Region都是基于复制算法去垃圾回收的,将一个Region内的存活对象放入另一个Region中,再把这个Region的垃圾对象给清理掉,这样就不会产生内存碎片了。清理掉垃圾对象的Region会不断的空闲出来,一旦达到了堆内存的5%,就会停止该次的混合回收。

下一个参数 -XX:G1MixedGCLiveThresholdPercent,默认值为85,意思是如果一个Region中的存活对象大于Region大小的85%的话,就不去回收这个Region,否则回收时将85%的存活对象放入另一个Region中,得不偿失。

在进行混合回收的时候,无论时年轻代还是老年代都是基于复制算法去回收Region的,一旦出现Region拷贝的过程中没有空闲的Region可以存放存活对象的时候,就会触发一次失败。然后停止工作线程,切换为单线程进行标记,清理和压缩整理,空闲出一部分的Region,这个过程会非常慢,所以要尽量避免这种情况的发生。

4.参数详解

G1垃圾收集器的重要选项的默认值

选项和默认值

选项

-XX:G1HeapRegionSize=n

设置G1区域的大小。该值为2的幂,范围为1 MB到32 MB。目标是根据最小Java堆大小具有大约2048个区域。

-XX:MaxGCPauseMillis=200

为所需的最大暂停时间设置目标值。默认值为200毫秒。指定的值不适合您的堆大小。

-XX:G1NewSizePercent=5

设置要用作年轻代大小的最小值的堆百分比。默认值为Java堆的5%。

这是一个实验性标志。有关示例,请参见如何解锁实验性VM标志。此设置代替-XX:DefaultMinNewGenPercent设置。

-XX:G1MaxNewSizePercent=60

设置堆大小的百分比,以用作年轻代大小的最大值。默认值为Java堆的60%。

这是一个实验性标志。有关示例,请参见如何解锁实验性VM标志。此设置代替-XX:DefaultMaxNewGenPercent设置。

-XX:ParallelGCThreads=n

设置STW工作线程的值。将的值设置n为逻辑处理器的数量。的值与n最多不超过8的逻辑处理器的数量相同。

如果逻辑处理器多于八个,则将n的值设置为逻辑处理器的大约5/8。除较大的SPARC系统外,这在大多数情况下均有效,其中n的值约为逻辑处理器的5/16。

-XX:ConcGCThreads=n

设置平行标记线的数量。设置n为并行垃圾回收线程数(ParallelGCThreads)的大约1/4。

-XX:InitiatingHeapOccupancyPercent=45

设置触发标记周期的Java堆占用阈值。默认占用率为整个Java堆的45%。

-XX:G1MixedGCLiveThresholdPercent=85

设置要包含在混合垃圾收集周期中的旧区域的占用阈值。默认占用率为85%。

这是一个实验性标志。有关示例,请参见如何解锁实验性VM标志。此设置代替-XX:G1OldCSetRegionLiveThresholdPercent设置。

-XX:G1HeapWastePercent=5

设置您愿意浪费的堆百分比。当可回收百分比小于堆垃圾百分比时,Java HotSpot VM不会启动混合垃圾回收周期。默认值为5%。

-XX:G1MixedGCCountTarget=8

设置标记周期后混合垃圾回收的目标数量,以收集最多包含G1MixedGCLIveThresholdPercent实时数据的旧区域。默认值为8个混合垃圾回收。混合馆藏的目标是在此目标数量之内。

-XX:G1OldCSetRegionThresholdPercent=10

设置在混合垃圾收集周期中要收集的旧区域数的上限。缺省值为Java堆的10%。

-XX:G1ReservePercent=10

设置保留内存的百分比以使其保持空闲状态,以减少空间溢出的风险。默认值为10%。当您增加或减少百分比时,请确保将总Java堆调整为相同的数量。

 

推荐建议:

 

  • Young Generation Size: Avoid explicitly setting young generation size with the -Xmn option or any or other related option such as -XX:NewRatio. Fixing the size of the young generation overrides the target pause-time goal.
  • Pause Time Goals: When you evaluate or tune any garbage collection, there is always a latency versus throughput trade-off. The G1 GC is an incremental garbage collector with uniform pauses, but also more overhead on the application threads. The throughput goal for the G1 GC is 90 percent application time and 10 percent garbage collection time. When you compare this to Java HotSpot VM's throughput collector, the goal there is 99 percent application time and 1 percent garbage collection time. Therefore, when you evaluate the G1 GC for throughput, relax your pause-time target. Setting too aggressive a goal indicates that you are willing to bear an increase in garbage collection overhead, which has a direct impact on throughput. When you evaluate the G1 GC for latency, you set your desired (soft) real-time goal, and the G1 GC will try to meet it. As a side effect, throughput may suffer.
  • Taming Mixed Garbage Collections: Experiment with the following options when you tune mixed garbage collections. See "Important Defaults" for information about these options:
     
    • -XX:InitiatingHeapOccupancyPercent
      For changing the marking threshold.
    • -XX:G1MixedGCLiveThresholdPercent and -XX:G1HeapWastePercent
      When you want to change the mixed garbage collections decisions.
    • -XX:G1MixedGCCountTarget and -XX:G1OldCSetRegionThresholdPercent
      When you want to adjust the CSet for old regions.

 

二、生产环境情况简述

1.JVM参数配置情况

-Xms2048m -Xmx2048m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -XX:MaxGCPauseMillis=200

2.服务器配置情况

3.应用情况

对象产生情况: 

内存占用情况: 

GC停顿时间: 

Heap after GC: 

April 1 - April 30: 

 

三、问题发现以及解决

问题一:内存占用过高,并且钉钉提示预警

问题观察

通过观察GC日志与内存监控发现,非年轻代内存只增不减

问题分析

服务器内存3.5G

项目内存Young + Old + Meta space=2.25G

垃圾收集器使用G1并配置回收时间200ms

项目稳定时年轻代大约占用1200m,老年大大约占用800m

从GC日志发现在运行期间从未出现过mixGC只是在不断的youngGC。通过查看原 有的JVM参数发现未配置-XX:InitiatingHeapOccupancyPercent,那么此参数为默认值 45%,即老年代占用为900M时触发mixGC。所以触发mixGC时老年代必然扩容且空余 极少内存,通过计算和观察服务器剩余内存得知当老年代空余内存少于三四百M时会 触发钉钉预警。此时,相关人员便会手动重启项目。

通过分析GC日志得出平均每次youngGC所占用的时间为10ms,且最大峰值不超 过50ms。但此时不建议减少-XX:MaxGCPauseMillis,因为这能够让mixgc多回收一点垃 圾,也可防止造成多轮mixgc浪费资源。

总结:JVM触发mixGC的参数值不匹配系统的实际运行情况导致从不触发mixGC去清除老年代对象,这是导致内存只增不减的最根本原因。

解决方案:

-XX:InitiatingHeapOccupancyPercent=20 (需持续观测,可能还可以再调大一点点),这个参数的意思是在老年代占用JVM的配置Heap超过20%时会触发mixGC,即老年代超过409.6M时会触发mixGC回收老年代、年轻代以及大对象

 

问题二:时而出现内存暴涨的情况(其实也不是很暴)

问题观察:

据说是因为接入报表之后才出现的这种情况。通过观察GC日志与内存监控发现确实存在老年代内存突然一下上涨百M的情况,且时间不定没有规律。

问题分析:

  1. 服务刚启动时,Eden较小如此时进行报表或涉及较多计算的操纵时可能出现eden放不下而将数据直接放入老年代的情况
  2. 一次加载报表的时间较长,在此期间被引用对象无法释放造成对象回收成本增大。极端情况下甚至会反向收缩Eden区导致对象进一步都放入老年代。另一种情况是对象动态年龄判断发现超过S区的50,数据进入老年代,或者极端时导致S区放不下扩容,数据直接进入老年代。

总结:内存突然上涨的根本原因是因为报表长时间查询会导致部分数据直接进入老年代

解决方案:

  1. 调整Eden的初始值大小 -XX:G1NewSizePercent=5为默认值。由于系统稳定时Eden区大小为1200M左右,且youngGC平均回收时间为10ms远远小于额定200ms。所以不妨将其设置为600M(-XX:G1NewSizePercent=30)或更高(但不建议超过1000M)
  2. 调整S区的大小-XX:SurvivorRatio=8(需要启动实验)

不建议调整这两个参数或者说必要性不大且存在一定风险,对于G1垃圾收集器存在暂停时间的概念,每次GC的时长都会尽量避免超出这个阈值。所以G1会自动调节Eden区、S区和Old区的大小来满足这个阈值,并且G1并不物理隔离这些内存分区只是逻辑隔离而已。所以Eden区如果太小那么G1会自动扩容,某些情况下如果S区太小也会自动扩容。系统稳定时G1也能够将S区缩小至稳定值。如果配置了这两参数将会限制G1的发挥,并且就算有较多数据进入老年代也可以通过mixGC将其清除。并且mixGC不等同于fullGC并不会出现时间较长的STW且其会满足回收时长阈值的要求,如果超过阈值那么会进行多轮mixgc对于系统的影响也不大。

 

问题三:频繁出现Humongous Allocation并且mixGC无法清除任何垃圾对象,并且通过观察服务器内存与应用GC日志发现有堆外内存泄漏的情况

问题观察

出现时间不规律,并且自5.15号之后尤为突出

问题分析一:

  1. 频繁出现Humongous Allocation,系统heap设置为2048M,Region配置为2048个,所以每个Region的大小为1M。且已知目前系统中无图片与文件上传经过后端,可能导致此结果的情况猜测为报表,
  2. 此外出现mixGC无法清除任何垃圾对象的现象。目前生产环境使用的是openjdk1.8.0_252版本,大对象可能只有在FullGC的情况下才能被清除,仅仅通过mixGC也许是不行的。这个结论和jdk版本有关,某些版本确实是这种情况。但是测试环境使用的是oracle jdk1.8.0_111,这个版本在mixGC时期即可清除大对象。

解决方案:

  1. 降低Region的个数来增大每个Region的容量,以此减少大对象的分配。
  2. 将openjdk升级为oraclejdk,使其可以在mixGC时期清理将大对象清理掉。

经测验-无法解决mixGC无法清除垃圾的情况

 

问题分析二:

  1. mixGC无法清除垃圾,说明垃圾对象还有引用存在。可能是由于代码问题导致的内存泄漏。

解决方案:

  1. 检查5.15号前的上线代码提交记录,观察代码并于代码提交者进行沟通。检查代码无明显逻辑问题。

 

问题分析三:

  1. Dump信息分析,通过jvisualVM对内存进行分析,打开类按大小倒序排列。发现byte[]大小有些异常占用了过高内存。

          

         查看实例具体信息发现有很多大小为10M的byte[]对象,将byte[]中的数据打印出来发现其实是http请求的Header,并且里面的内容大部分是空的未被使用到。

          

        我们查找其最近的根节点发现为HeapByteBuffer,持有byte[]的HeapByteBuffer是NIO相关的类,但是经过排查系统中并无Netty之类的程序。所以,推测其是由于Tomcat的不合理配置导致的。

          

       登录服务器查看项目的配置文件发现tomcat有一处配置不合理,其大小也正好符合实例详情中byte[]的大小,这应该就是问题所在。

          

总结:错误的配置了max-http-header-size=10M。容器初始化处理请求的线程池时,每个线程都会申请10M大小的byte[]对象,并且请求处理线程的生命周期和应用的生命周期相同。这意味着线程持有的 byte[]对象在整个服务周期中是一直存活的。一般线程池的规模少说也在几十个(生产环境据观察有三四十个个),也意味着服务正常工作时,几百兆的堆内存(也可能是堆外内存)会被请求处理线程一直占用。这就解释了为何堆外内存会出现泄漏,并且mixGC无法清除任何垃圾的情况(解释了问题3和1)。如果遇上系统使用高峰,也不难理解内存会突然上涨(解释了问题2)

解决方案:

查看源码发现其默认大小为2M,结合业务实际情况来说足够用了,所以删除此项配置使用默认值。

总结

根据服务器情况与钉钉告警阈值情况调整配置 -XX:InitiatingHeapOccupancyPercent=25(还有调整空间)使G1提前进入MixGC,并在其触发钉钉告警前回收垃圾。

调整配置 -XX:MaxGCPauseMillis=500使其每次的MixGC可以回收尽可能多的垃圾对象,且实际上不影响频率最高的YoungGC,因为通过观察GC日志得知YoungGC的情况99%是因为Eden区满而触发的并且平均时长为10ms。

删除Tomcat配置max-http-header-size=10M使用默认值替代之,否则容器线程池会占用过多无效内存造成泄漏。

 

参考文献:

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html

https://www.oracle.com/technical-resources/articles/java/g1gc.html

https://blog.csdn.net/coderlius/article/details/79272773

https://juejin.cn/post/6844903953004494856

深入理解JAVA虚拟机

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM (Java Virtual Machine) G1 (Garbage-First) 垃圾收集器是一种用于 Java 应用程序的垃圾收集算法。它是自JDK 7u4版本后引入的一种全新的垃圾收集器G1垃圾收集器的设计目标是为了解决传统的分代垃圾收集器可能遇到的一些问题,如停顿时间长、内存碎片化等。它采用了一种基于区域的垃圾收集方式,可以将内存划分为多个大小相等的区域,每个区域可以是Eden、Survivor或Old区。 G1垃圾收集器的工作原理如下: 1. 初始标记(Initial Mark):标记所有从根对象直接可达的对象。 2. 并发标记(Concurrent Mark):在并发执行程序的同时,标记那些在初始标记阶段无法访问到的对象。 3. 最终标记(Final Mark):为并发标记阶段中发生改变的对象进行最终标记。 4. 筛选回收(Live Data Counting and Evacuation):根据各个区域的回收价值来优先回收价值低的区域。 G1垃圾收集器具有以下特点: - 并发执行:在执行垃圾收集过程时,尽可能减少应用程序的停顿时间。 - 分区回收:将整个堆划分为多个区域,可以根据需要优先回收垃圾较多的区域,从而避免全堆回收带来的长时间停顿。 - 内存整理:G1垃圾收集器会对内存进行整理,减少内存碎片化,提高内存利用率。 需要注意的是,G1垃圾收集器并不适用于所有情况。在特定的场景下,如大堆情况下的长时间运行、对延迟要求非常高的应用等,可能需要考虑其他垃圾收集器的使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值