G1垃圾回收器介绍和线上实践

原创
2022/05/21 20:34
阅读数 2.5K

本文通过与CMS工作过程进行对比,来介绍G1的工作过程和两者差异。同时介绍G1垃圾回收器在网易支付线上的实践,实践过程中遇到的问题、问题解决思路和最终结果。最终通过切换到G1垃圾回收器和参数调优,实现整体gc次数和时间相较之前均降低50%以上。

一、前言

Java语言相较于C++等语言,一个显著的特点是垃圾回收机制,允许程序员在编写程序时无需考虑内存管理,统一由底层的垃圾回收机器进行垃圾回收。但是垃圾回收器在回收垃圾时,会对应用线程造成停顿,影响应用的性能。

在Java应用调优中,核心的两个指标为:响应时间和吞吐量。

1.1 响应时间

响应时间指的是应用对请求的响应时间,如:

  • 一个桌面应用对时间的响应时间

  • 网站返回一个页面的时间

  • 数据库查询结果返回时间

对于专注于最小响应时间的应用,长时间停顿是无法接受的。

1.2 吞吐量

吞吐量专注于应用在一段时间的的最大工作量。如:

  • 给定时间内完成的事物数

  • 每小时批处理程序能够完成的任务

  • 每小时数据库可以完成的查询操作数

较长停顿时间在此情况下是可以接受的。比起低响应时间,吞吐量优先应用更看重一段时间内的表现。

1.3 总结

基于两个指标,可得出评估一个垃圾回收器在实际业务中关键的是:单次最大停掉时间和周期内的停顿次数。

注:本文介绍的G1主要是基于JDK8进行分析,在后续的JDK版本中,有较多优化,比如JDK10 退化后的Full GC支持多线程等。

二、G1垃圾回收器

Garbage-First(G1)收集器时一款服务端垃圾收集器,主要针对多处理器、大内存环境设计。在尽可能满足GC停顿时间目标的同时获取更高的吞吐量。在JDK7u4版本开始支持

2.1 基本介绍

2.1.1 设计目标

  • 像CMS那样做到并发GC,提高GC并行和并发表现

  • 没有内存碎片问题,内存整理过程不需要延长GC时间,不需要虚拟机停顿STW

  • 可预测的GC停顿时间

  • 更高的吞吐量

  • 在不增加堆内存大小下更好地利用堆内存

G1的远期目标时替代CMS+ParNew(自适应的、分代的、停顿-复制、标记-清理并发垃圾回收器)组合。

2.1.2 优点

与CMS相比,G1优点包括:

  • G1是内存整理虚拟机,G1通过对堆内存划分区域(region),分区管理,避免使用细粒度空闲列表来实现高效内存整理;

  • G1提供比CMS更加可预测的GC停顿时间,并允许用户设定停顿时间目标。

2.1.3 推荐应用场景

G1的设计初衷是为用户提供大内存、低GC停顿时间的应用解决方案。

如果应用正在使用CMS或ParallelOld且面临以下问题,推荐将应用迁移至G1

  • FullGC发生频繁或总时间过长

  • 对象分配率或对象升级至老年代的比例波动较大

  • 较长的垃圾收集或内存整理停顿(大于0.5至1秒)

注意:

  • 如果应用没有上述问题,可以不需要切换到G1。

  • 切换到G1不要求更新JDK版本。

2.2 实现概览

2.2.1 老式垃圾回收器

老式垃圾回收器(Serial,Parallel,CMS)统一将堆分成大小固定的三部分:新生代、老年代和永久代。整体划分类似如下图所示:

2.2.2 G1 内存划分

在G1垃圾回收器中,使用了不同的内存划分方法,其将内存划分为不同小的区域(region),每个区域在虚拟内存上是连续的。同时在实际使用中,每个区域会像老式垃圾回收器一样,标记该region为eden、survivor和old,但是某个区域不是固定属于区(eden等),其余依据实际使用动态地去调整。

G1收集器将堆均分成大小相同的区域,每个区域的大小为1M~32M之间,最大支持2048个区域,因此G1最大可支持的内存为64G。

可参考源码:

https://github.com/wu560130911/jdk/blob/jdk8-b119/hotspot/src/share/vm/gc_implementation/g1/heapRegion.cpp#L139


  // Minimum region size; we won't go lower than that.
  // We might want to decrease this in the future, to deal with small
  // heaps a bit more efficiently.
  static const size_t MIN_REGION_SIZE = 1024 * 1024;

  // Maximum region size; we don't go higher than that. There's a good
  // reason for having an upper bound. We don't want regions to get too
  // large, otherwise cleanup's effectiveness would decrease as there
  // will be fewer opportunities to find totally empty regions after
  // marking.
  static const size_t MAX_REGION_SIZE = 32 * 1024 * 1024;

  // The automatic region size calculation will try to have around this
  // many regions in the heap.
  static const size_t TARGET_REGION_NUMBER = 2048;

在不同堆大小写的区域大小如表格:

堆大小 region大小
heap < 4GB 1M
4GB <= headp < 8GB 2M
8GB <= headp < 16GB 4M
16GB <= headp < 32GB 8M
32GB <= headp < 64GB 16M
heap = 64GB 32M

2.2.3 回收概要

G1垃圾收集过程与CMS类似。G1在堆内存中并发地对所有对象进行标记,决定对象的可达性。经过全局标记,G1了解哪些区域几乎是空的,然后优先收集这些区域,这就是GarbageFirst的命名由来。

G1将垃圾收集和内存整理活动专注于那些几乎全是垃圾的区域,并建立停顿预测模型来决定每次GC时回收哪些区域,以满足用户设定的停顿时间。

对于区域的回收通过复制算法实现。在完成标记清理后,G1将这几个区域的存活对象复制到一个单独区域中,实现内存整理和空间释放。这一过程通过多线程并行进行来降低停顿时间,提高吞吐量。通过这样的方式,G1在每次GC过程中持续清理碎片,控制停顿时间满足用户要求。

这是过去虚拟机无法做到的,CMS不清理内存碎片(除非通过虚拟机参数设置,在每次或多次FullGC后进行整理),ParallelOld进行全堆整理,会导致较长的停顿时间。

G1不是实时垃圾收集器,它会尽量让停顿时间低于用户设置的停顿时间目标但不能保证一定如此。

G1根据历史垃圾收集监测数据来 预测每个区域的回收时间,然后根据用户设定的目标停顿时间决定每次GC时可以回收哪些区域。G1通过 这种方式建立比较精确的区域回收时间预测模型。

注意:G1有并发阶段(与应用线程并行,无停顿时间)和并行阶段(多线程,应用工作线程停顿)。FullGC仍是单线程,如果调优合适,可避免出现FullGC。

2.2.4 特殊内存占用

如果从ParallelOldGC或CMS迁移至G1,你会发现JVM线程占用内存增大。增大的部分主要与'accouting'数据结构有关,如Remembered Sets(RSets)和Collection Sets(CSets)。

  • Remembered Sets RSets跟踪指向某个区域的对象引用。每个区域对应一个RSet。RSets对整体内存占用的影响少于5%;

  • Collection Sets CSets 在一次GC中将被回收的区域集合。所有CSet区域中的存活对象都会被移动到新的区域中,这些区域可以是Eden区、survivor区或老年代。CSets对JVM内存占用影响少于1%;

2.3 垃圾回收过程介绍

G1的垃圾回收与CMS回收过程比较类似,为了方便大家了解G1的回收过程或更直观清楚CMS与G1的区别,接下来将分别介绍CMS和G1的垃圾回收过程。

2.3.1 CMS回收过程

CMS(并发低停顿收集器)收集老年代。通过将大部分垃圾回收工作与应用线程并发进行,尽可能降低停顿时间。通常CMS不回去复制和整理内存,GC过程不会移动对象。

阶段 描述
初始标记(STW) 标记GCRoot直接引用的的老年代对象,包括从新生代引用的对象,停顿时间相比minorGC非常短
并发标记 遍历老年代对象,此时应用线程并发执行。从已经标记的对象和GCRoot开始标记所有可达对象。所有并发阶段新分配的对象或从新生代升级到老年代的对象都标记为可达对象。会存在对象漏标记
重新标记(STW) 暂停应用现成,修正并发标记阶段由于应用线程运行导致漏标记的对象。在JDK1.5之后,为减少对应用运行的影响,会在前面增加并发预清理阶段。
并发清除 收集标记为不可达的对象。对象内存空间被添加到空闲列表中等待分配,这些回收对象的空间可以被合并。存活对象在这一阶段不会移动
重置 重置GC数据结构,为下次GC做准备

2.3.2 G1回收过程

在2.2节中已经介绍G1会将内存划分为很多区域,每个区域大小相等,这些区域被映射为逻辑上的Eden、survivor和老年代区域,相同类型区域地址可以不连续。在实际中,除了上述的三个区域类型,还会存在一种大对象区,其用来存储对象超过region大小一半及以上的对象,可能会同时占用多个连续的区域类存放。

在G1垃圾回收过程,会分为新代码垃圾回收(yong gc)和老年代回收(mixed gc),在极端场景下会退化为单线程的Serial Old垃圾收集器。

2.3.2.1 新生代回收

在分配一般对象(非巨型对象)时,当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次younggc会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。到Old区的标准就是在PLAB中得到的计算结果。因为YoungGC会进行根扫描,所以会stop the world。

YoungGC的回收过程如下:

  • 根扫描,跟CMS类似,Stop the world,扫描GC Roots对象。

  • 处理Dirty card,更新RSet.

  • 扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。

  • 拷贝扫描出的存活的对象到survivor2/old区

  • 处理引用队列,软引用,弱引用,虚引用

2.3.2.2 MixGC混合收集

G1回收器在对内存的老年代区域执行以下阶段。注意这些阶段也包括新生代的回收。

阶段 描述
初始标记STW 捎带一次youngGC。标记可能引用了老年代区域对象的survivor区域(根区域) 在GC日志中打印为GCpause(young)(inital-mark)
根区域扫描 扫描根区域中指向老年代的引用。与应用线程并发。此阶段完成后才可以进行youngGC
并发标记 全堆扫描存活对象。与应用现成并发。这一阶段可以被youngGC打断。计算各区域活跃度(回收优先级)的所需的信息在这个阶段统计
重新标记STW 完成堆中所有存活对象的扫描,使用SATB(snapshot-at-the-beginning)算法。空区域被回收,计算出所有区域活跃度
清除 统计存活对象和空区域(STW),更新RSets(STW),重置空区域,加入空白列表(并发)
复制STW 将存活对象移动至未使用区域

总结:

  • 并发标记阶段

  • 区域活跃度信息的统计与应用线程并发进行

  • 活跃度信息决定了那个区域最先在清理阶段被回收

  • 没有CMS的清理阶段

  • 重新标记阶段

  • SATB算法比CMS使用的算法更快

  • 空区域在这个阶段被回收

  • 复制/清理阶段

  • 新生代和老年代同时被回收

  • 老年代根据活跃度确定回收优先级

Rsets

G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用是使用Remembered Set来避免扫描全堆。

G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之间(在分代中例子中就是检查是否老年代中的对象引用了新生代的对象),如果是便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。

SATB

G1 使用的是 SATB 标记算法,主要应用于垃圾收集的并发标记阶段,解决了CMS 垃圾收 集器重新标记阶段长时间 Stop The World(STW) 的潜在风险。其算法全称是 Snapshot At The Beginning,由字面理解,是垃圾回收器开始时活着的对象的一个快照。

通过 “根集合”穷举可达对象得到的,穷举过程中采用了三色标记法:

  • 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。

  • 灰:对象被标记了,但是它所在的Field还没有被标记或标记完(可达对象还未被标记)。

  • 黑:对象被标记了,且它的所有Field也被标记完了。

所以,漏标的情况只会发生在白色对象中,且满足以下任意一个条件:

  • 并发标记时,应用线程给一个黑色对象的引用类型字段赋值了该白色对象

  • 并发标记时,应用线程删除所有灰色对象到该白色对象的引用

SATB 利用 write barrier 将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根 Stop The World 地重新扫描一遍即可避免漏标问题。因此 G1 Remark阶段 Stop The World 与 CMS 了的remark有一个本质上的区别,那就是这个暂停只需要扫描有 write barrier 所追中对象为根的对象, 而 CMS 的remark 需要重新扫描整个根 集合,因而CMS remark有可能会非常慢。

2.3.3 G1最佳实践

使用G1垃圾回收器,可使用参数即可开启:-XX:+UserG1GC。官方依据其特性总结了如下最佳实践或实践调优思路。

2.3.3.1 不设置新生代大小

设置新生代大小-Xmn会影响G1的回收表现。

  • G1不在遵守目标停顿时间,即设置新生代大小后,目标停顿时间失效。

  • G1不能按需扩展或收缩新生代空间。

2.3.3.2 响应时间指标

不要根据平均响应时间ART设置MaxGCPauseMillis参数,这个值设置为90%的用户发起请求时的等待时间不会超过的值。此参数会影响年轻代大小的分配,进而影响mixed gc的频率。

2.3.3.3 转移失败

在GC过程中,堆内存区域耗尽,新生代对象晋升老年代失败。堆内存由于已经达到最大而无法扩张。在GClog中打印to-space overflow日志。这会耗费大量资源。

  • GC仍要继续,所以空间必须被释放。

  • 拷贝失败的对象必须留在原区域。

  • 这一过程中,CSets中区域的RSets如果发生变化,RSets需要重新生成。

  • 这些步骤都非常耗费资源。

避免转移失败的措施:

  • 增加堆内存大小

  • 提高-XX:G1ReservePercent,默认值是10

  • G1为堆内存设置虚拟使用上限,预留一部分空间防止to-space情况出现

  • 提前进行标记阶段

  • 提高标记阶段的线程数,-XX:ConcGCThreads

2.3.3.4 常见参数一览

参数与默认值 说明
-XX:+UserG1GC 使用G1收集器
-XX:MaxGCPauseMillis=200 设置最大停顿时间,软目标,虚拟机不保证总是达到要求
-XX:InitiatingHeapOccupancyPercent=45 当堆内存使用率达到多少时启动一次GC周期。GC周期针对整个堆内存而不是某个年龄代。设为0时表示持续进行GC周期
-XX:NewRatio=2 新生代与老年代大小比例
-XX:SurvivorRatio=8 Eden区与survivor区大小比例
-XX:MaxTenuringThreshold=15 新生代存活过多少次youngGC后晋升老年代
-XX:ConcGCThreads 并发垃圾回收器使用的线程数,默认值根据JVM运行平台不同而变化
-XX:ParallelGCThreads 在并行阶段垃圾回收器线程数
-XX:G1ReservePercent=10 预留多少个区域,防止出现转移失败导致to-space overflow
-XX:G1HeapRegionSize G1将堆内存划分为大小相同的多个区域。区域大小由这个参数确定。默认值与堆内存大小有关。不小于1M,不大于32M。总数2048
-XX:-G1UseAdaptiveIHOP 关闭G1的自动IHOP分析机制
-XX:+AlwaysPreTouch 启动时分配好机器的物理内存;(JDK8以前单线程初始化, 8u60后此参数生效 )
-XX:+ParallelRefProcEnabled 并行的处理 Reference 对象【proc ref时间较长时,可以开启此参数
-XX:G1NewSizePercent=5 新生代比例下限【需要依据实际情况进行调整,否则会导致young gc频繁
-XX:G1MaxNewSizePercent=60 新生代比例上限【新生代gc耗时过长时调下比例可降低耗时
-XX:G1MixedGCCountTarget=8 指定一个周期内触发Mixed GC最大次数【mixgc时间过长可调优此参数
-XX:G1OldCSetRegionThresholdPercent=10 指定每轮Mixed GC回收的Region最大比例
完整G1参数列表可参考:https://github.com/wu560130911/jdk/blob/jdk8-b119/hotspot/src/share/vm/gc_implementation/g1/g1_globals.hpp
更多调优思路可参考官方文档:https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector-tuning.htm#JSGCT-GUID-90E30ACA-8040-432E-B3A0-1E0440AB556A

三、线上实践

网易支付线上目前统一使用ParNew+CMS垃圾回收器,同时有一套统一的标准化参数。但是在实际运行过程中,经常发生因CMS内存碎片等原因导致fullgc时间较长和频率较高,给线上业务造成一定的影响,以此为契机,调研G1并在线上进行实践,最终实现缩短50%以上的垃圾停顿时间和减少一半以上的垃圾停顿次数。

3.1 CMS的坑

单独一节介绍CMS坑,主要是为还在使用CMS垃圾回收器的业务参考并检查是否存在此问题。

在使用CMS垃圾回收器时,必须显式地设置年轻代大小,否则会因CMS垃圾回收器内置机制,自动化设置很小的年轻代大小。

CMS默认年轻代大小计算规则为:young heap size = min( 64cpus13/10M, heapsize/3)。

假设堆大小为4G,4C机器,则默认新生代只有:64413/10 = 332.8M,则EDEN默认只有266.24M

3.2 G1调优实践

基于上述G1的介绍,针对线上部分应用灰度使用如下G1参数:

-XX:+UseG1GC -XX:MaxGCPauseMillis=100 –XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xms4096m -Xmx4096m

3.2.1 ref proc耗时长

发现针对灰度的大部分应用在一周内的gc次数明显减少一半,但是平均单次gc时间却有上升,通过查看gc日志:

122694.975: [GC pause (G1 Evacuation Pause) (young), 0.1019143 secs]
   [Parallel Time: 15.8 ms, GC Workers: 4]
      [GC Worker Start (ms): Min122694974.9Avg122694975.1Max122694975.4, Diff: 0.5]
      [Ext Root Scanning (ms): Min0.8Avg1.2Max1.4, Diff: 0.6Sum4.8]
      [Update RS (ms): Min1.2Avg1.3Max1.4, Diff: 0.2Sum5.1]
         [Processed Buffers: Min5Avg11.2Max17, Diff: 12Sum45]
      [Scan RS (ms): Min0.4Avg0.5Max0.5, Diff: 0.1Sum1.9]
      [Code Root Scanning (ms): Min0.2Avg0.3Max0.7, Diff: 0.5Sum1.4]
      [Object Copy (ms): Min11.9Avg12.1Max12.2, Diff: 0.3Sum48.5]
      [Termination (ms): Min0.0Avg0.0Max0.0, Diff: 0.0Sum0.1]
         [Termination Attempts: Min10Avg14.8Max19, Diff: 9Sum59]
      [GC Worker Other (ms): Min0.0Avg0.1Max0.1, Diff: 0.1Sum0.3]
      [GC Worker Total (ms): Min15.1Avg15.5Max15.7, Diff: 0.5Sum62.0]
      [GC Worker End (ms): Min122694990.5Avg122694990.6Max122694990.6, Diff: 0.1]
   [Code Root Fixup: 0.2 ms]
   [Code Root Purge0.0 ms]
   [Clear CT: 0.5 ms]
   [Other: 85.4 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 83.0 ms]
      [Ref Enq: 0.4 ms]
      [Redirty Cards: 0.3 ms]
      [Humongous Register0.1 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 1.2 ms]
   [Eden: 2430.0M(2430.0M)->0.0B(2428.0M) Survivors: 26.0M->28.0Heap2529.8M(4096.0M)->101.9M(4096.0M)]
 [Times: user=0.15 sys=0.00real=0.10 secs]

发现耗时基本都在ref proc步骤中,这个步骤主要的工作内容为:

Ref Proc - Processes any soft/weak/final/phantom/JNI references discovered by the STW reference processor

通过在jvm增加参数(-XX:+PrintReferenceGC),查看这个步骤是哪些引用处理耗时过长导致:

3198.886: [GC pause (G1 Evacuation Pause) (young)3198.927: [SoftReference, 0 refs, 0.0000643 secs]3198.928: [WeakReference, 134 refs, 0.0000358 secs]3198.928: [FinalReference, 27351 refs, 0.0674950 secs]3198.995: [PhantomReference, 0 refs, 20 refs, 0.0000200 secs]3198.995: [JNI Weak Reference, 0.0000134 secs], 0.1117460 secs]
   [Parallel Time: 39.1 ms, GC Workers: 4]
      [GC Worker Start (ms): Min3198886.5Avg3198886.5Max3198886.6, Diff: 0.1]
      [Ext Root Scanning (ms): Min1.5Avg2.4Max5.0, Diff: 3.5Sum9.8]
      [Update RS (ms): Min0.0Avg1.3Max1.8, Diff: 1.8Sum5.2]
         [Processed Buffers: Min0Avg8.0Max17, Diff: 17Sum32]
      [Scan RS (ms): Min0.6Avg0.9Max1.1, Diff: 0.5Sum3.6]
      [Code Root Scanning (ms): Min0.1Avg1.9Max3.4, Diff: 3.2Sum7.7]
      [Object Copy (ms): Min30.9Avg32.2Max33.7, Diff: 2.9Sum128.9]
      [Termination (ms): Min0.0Avg0.0Max0.0, Diff: 0.0Sum0.0]
         [Termination Attempts: Min2Avg3.8Max6, Diff: 4Sum15]
      [GC Worker Other (ms): Min0.1Avg0.1Max0.1, Diff: 0.0Sum0.3]
      [GC Worker Total (ms): Min38.8Avg38.9Max38.9, Diff: 0.1Sum155.5]
      [GC Worker End (ms): Min3198925.4Avg3198925.4Max3198925.4, Diff: 0.1]
   [Code Root Fixup: 1.7 ms]
   [Code Root Purge0.1 ms]
   [Clear CT: 0.4 ms]
   [Other: 70.6 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 68.0 ms]
      [Ref Enq: 0.4 ms]
      [Redirty Cards: 0.2 ms]
      [Humongous Register0.2 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 1.3 ms]
   [Eden: 1870.0M(1870.0M)->0.0B(2406.0M) Survivors: 42.0M->50.0Heap1941.7M(4096.0M)->80.7M(4096.0M)]
 [Times: user=0.21 sys=0.02real=0.12 secs]

可以看到,主要耗时的阶段为:[FinalReference, 27351 refs, 0.0674950 secs],基于此需要知道堆中存在哪些final引用,dump下堆,使用如下oql:

var counts = {};
heap.forEachObject(function(fobject){
  if(fobject.referent!=null)
  {
    var className = classof(fobject.referent).name;
              if (!counts[className]) {
                  counts[className] = 1;
              } else {
                  counts[className] = counts[className] + 1;
              }
  }
},'java.lang.ref.Finalizer',true);

sort(map(counts,function(key,value){
return {string: value, count: counts[value]};
}), 'rhs.count - lhs.count');

查询结果如下:

主要为socket对象,在java8的SocketSocketImpl里实现了Object的finalize方法,防止socket连接遗忘释放资源,而进行兜底进行释放。

在JDK9版本中,将不再重新此方法,但是oracle在jdk8之后特定版本会对企业进行收费,支付侧无法再往上升级,通过查找官方文档发现,可通过并行引用处理来解决。

https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector-tuning.htm#GUID-40B64CD4-9844-4E3E-A0BB-81556AC04C74

Reference Object Processing Takes Too Long
Information about the time taken for processing of Reference Objects is shown in the Ref Proc and Ref Enq phases. 
During the Ref Proc phase, G1 updates the referents of Reference Objects according to the requirements of their 
particular type. In Ref Enq, G1 enqueues Reference Objects into their respective reference queue if their 
referents were found dead. If these phases take too longthen consider enabling parallelization of these phases 
by using the option -XX:+ParallelRefProcEnabled.

通过增加此参数后(-XX:+ParallelRefProcEnabled),单次gc时间明显远低于原先的整体时间。其中一个应用一段时间的gc对比如下所示:


ygc次数 ygc时间 ygc平均时间 fgc次数 fgc时间 fgc平均时间
ParNew+CMS 2475 151074 61.04 6 4533 755.50
G1优化后 1163 54793 47.11 0 0 0

次数减少53.01% 总体时间减少63.73% 单次时间减少22.82%


3.2.2 频繁young gc

运行一段时间后,发现在触发mixedgc时,会触发大量的young gc,同时young gc的大小突降到204MB(此部分gc日志忘记留存,此次不贴)。

经过排查后发现,基于g1的可预期停顿时间算法,其会对各个区的大小进行动态分配,以满足其预期的时间,在触发mixed gc时后,其针对预测的时间会结合mixed gc的时间,进而会影响后续的年轻代大小分配(在young gc和mixed gc均会触发年轻代大小的动态调整,此块是依据动态运行时数据进行调整的,有兴趣可去查看调整算法),因此在极端场景下,年轻代大小会被调整为默认的最小值:-XX:G1NewSizePercent=5(在堆为4GB时,会被划分为2048个region,每个region为2M,进而年轻代大小为20480.052=204MB)。

要调整默认的年轻代最小比例,需要开启实验标识后才可进行设置,设置为如下后,基本未再触发因Mixed gc导致的频繁young gc。(注意年轻代比例需要依据实际业务进行设置,设置过大会导致gc停顿时间过长)

-XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=30

3.2.3 metaspace导致mixed gc

运行一段时间后,发现在堆内存原未达到阈值时,触发了mixed gc,通过查看gc日志后,发现为metaspace进行扩容时,触发了mixed gc。

在jdk8中,MetaspaceSize默认约为20MB,在应用启动过程中和运行过程中,实际使用超过大小时,会触发mixed gc进行扩容。依据应用实际情况,添加如下参数后,未再触发因metaspace扩容导致的mixed gc。

-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m

3.3 总结

以上为依据支付侧实际线上运行数据进行分析总结,最终调优得出的参数。通过对问题的分析和调优,线上未出现过fullgc,且整体gc次数和时间均降低50%以上,减少因gc导致线上的影响,保障线上服务的响应时间和吞吐量。

以下为线上实际使用的G1参数,可供大家参考,建议持续观察线上情况并进行动态调整,部分调优思路或经验也标注在2.3.3.4章节中

-XX:+UseG1GC -XX:MaxGCPauseMillis=100 –XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xms4096m -Xmx4096m
 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=30

注:在此参数中,年轻代的最小大小为:20480.32 = 1228MB

3.4 后续规划

后续将在支付内按照应用标准化参数推广落地,实现全业务切换到G1垃圾回收器。因不同业务类型的应用,gc停顿时间和次数会不同,为解决因参数设置不合理导致性能变差,将利用哨兵建立统一的JVM监控和监控模板,实现实时监测。同时采集各个应用的gc日志,通过对gc日志自动化解析和分析,给出当前存在的问题和优化方向。

-- End --

点击下方的公众号入口,关注「技术对话」微信公众号,可查看历史文章,投稿请在公众号后台回复投稿

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
3 收藏
0
分享
返回顶部
顶部