Parallel Collector 频繁 Full GC 解决

12 篇文章 1 订阅

Parallel Collector 频繁 Full GC 解决

一、发现问题

1.1 告警提示

【小赢告警】 [jvm告警] 10.0.193.95 2022-08-29 14:03:22
FGc count more than 3 times of last 5 minutes 10.0.193.166 of credit.afp_fund_project.center.

管理平台 发出告警,告警内容详细的指出了 IP 和 服务

二 查看 gc.log

2.1 通过 iTerm2 登陆目标机器

  进入 PASS 平台,查看 credit.afp_fund_project.center 服务 gc日志相关的 JVM 参数,发现配置如下:

  • -Xloggc:../log/gc.log :gc日志输出位置

  • -XX:+UseGCLogFileRotation:开启滚动日志(也就是日志文件循环使用)

  • -XX:NumberOfGCLogFiles=10 :滚动日志数量,gc.log.5.current 代表当前正在写入的日志文件

  执行如下命令,进入gc日志文件目录

cd /home/log/credit.afp_fund_project.center

在这里插入图片描述

2.2 查看 Full GC 日志

  执行如下命令:

grep "Full GC" gc.log.5.current

  命令结果输出如下:
在这里插入图片描述

2.3 日志解析

解析其中一条日志

2022-09-21T11:27:15.325+0800: 3527607.894: [Full GC (Ergonomics) [PSYoungGen: 416K->0K(90624K)] [ParOldGen: 113562K->71900K(115200K)] 113978K->71900K(205824K), [Metaspace: 128023K->128023K(1169408K)], 0.1923922 secs] [Times: user=0.37 sys=0.00, real=0.20 secs]
  • [PSYoungGen: 416K->0K(90624K)] :堆内存中年轻代回收情况,416K 回收之前对象占用的内存大小,0K 回收之后对象占用的内存大小,(90624K):整个年轻代可用容量
  • [ParOldGen: 113562K->71900K(115200K)]:堆内存老年代回收情况,113562K 回收之前对象占用的内存大小,71900K 回收之后对象占用的内存大小,(115200K):整个老年代可用容量
  • 113978K->71900K(205824K) :整个堆内存回收情况,113978K 回收之前对象占用的内存大小,71900K 回收之后对象占用的内存大小,(205824K):整个堆内存可用容量(PS:JDK8,堆内存 = 年轻代 + 老年代)
  • [Metaspace: 128023K->128023K(1169408K)]:元空间回收情况,128023K 回收之前类相关信息占用内存大小,128023K 回收之后类相关信息占用内存大小,(1169408K):整个元空间可用容量(PS:JDK8,元空间不在堆内存,而是跟本地内存有关)
  • 0.1923922 secs :整个 Full GC 阶段耗时
  • [Times: user=0.37 sys=0.00, real=0.20 secs]:GC 事件在不同维度的耗时
  1. user=0.37 :CPU维度,在垃圾回收期间,所有 CPU 总的耗时加起来 = 0.37
  2. sys=0.00:操作系统维度,操作系统调用或等待系统事件的时间,并不是真的 0,只是小数位省略了
  3. real=0.20 secs:应用程序维度,应用程序停顿时间 = 0.2 s,对于并行GC,这个数字应该接近 ( user + sys ) 除以垃圾收集器使用的线程数

注意!所选的垃圾回收器不一样,日志输出样式差别也不一样。credit.afp_fund_project.center 的JVM参数并没有指明使用哪款垃圾回收器,那么它用的就是JDK8默认垃圾回收器,Parallel Scavenge 收集器(新生代) + Parallel Old 收集器(老年代)

从多条日志上看

  1. 从触发频次上来看:

  几乎每 2 分钟会发生一次 Full GC,频繁的 Full GC 会严重降低服务吞吐量,局部造成接口耗时增加

鉴于目前服务堆内存才200M,Full GC 阶段耗时不会太大,维持在 160 ms - 200 ms

  1. 从多条日志数据上分析:

  触发 Full GC 的原因是由于老年代中对象占用内存达到阈值,从而导致 Full GC。进一步可理解为,每次 YGC 之后,都有对象从年轻代进入老年代,从而导致老年代占用内存越来越高。

  每次回收之后,老年代大约有 70497K 大小对象剩余,估计这些对象强引用一直存在,这部分内存无法被回收,这部分可以看着是 JDK 一些内置常驻堆内存的对象(类加载器、工具类等)和业务 class 对象、spring管理对象等,是不是还包括业务大缓存对象,这个还需进一步分析,因为 70497K 说实话不大。同时每次大约有 42078K 大小对象被回收,也就意味着这部分对象强引用只是短暂存在,经过 YGC 后逃逸到老年代。

JDK8 默认垃圾回收器不会单独回收老年代,如果老年代越来越大,只能触发 Full GC

三、初步排查、初步定位问题

  从上面的日志分析,我们可以把目光瞄准这2个方向

  1. 每次 Full GC 后老年代回收的 42078K 大小对象,它们为什么逃逸到了老年代?从三个分代假说来看,这种朝生夕灭到对象在年轻代就应该被回收。
  2. credit.afp_fund_project.center 服务配置了 -Xmx512m,实际情况是堆内存一直都达不到理想情况堆内存 = 512m,堆内存过小会引起频繁的 YGCFull GC 等一系列连锁问题

3.1问题一:垃圾对象逃逸

  这个章节会大量使用虚拟机性能监控、故障处理工具(jps、jstat、jinfo),详细可以看这篇文章 :虚拟机性能监控、故障处理工具

3.1.1 通过服务名找到PID

  执行如下命令

jps -l | grep "credit.afp_fund_project.center"

在这里插入图片描述

  credit.afp_fund_project.center 服务的 PID = 14036

3.1.2 监视新生代垃圾收集

  在监听之前,有必要说明下新生代对象晋升到老年代机制。新生代对象每经历依次 YGC,年龄会加一,当达到年龄阀值(TT)会直接进入老年代。阀值大小一般为15

这是标准的晋升机制,当然还要其他情况,触发老年代空间担保机制、大对象机制等,这里就不一一展开说明

执行如下命令,监视新生代垃圾收集状况

jstat -gcnew 14036 20

在这里插入图片描述

S0C:第一个幸存区容量
S1C:第二个幸存区的容量
S0U:第一个幸存区的已使用大小
S1U:第二个幸存区的已使用大小
TT:当前的年龄阀值
MTT:最大年龄阀值
DSS:期望的幸存区大小
EC:伊甸园区的容量
EU:伊甸园区的已使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间

这里有2个点我们需要重点关注下

3.1.2.1 TT = 1 不合理

  TT = 1,代表新生代的对象经过一次 YGC 后,年龄+1,再次发生 YGC 的话,如果强引用还在就会晋升到老年代。这个会导致很多本该在年轻代被回收的对象逃逸到老年代,从而导致老年代不断增大触发 Full GC

  Parallel GC 的动态年龄判断机制是会调整 TT的值,调整规则如下

  • young_gc_time > full_gc_time*1.1,则threshold降低。即 YoungGC 的时间太多,就降低 TenuringThreshold的值,让更多的对象进入老年代。
  • full_gc_time > young_gc_time*1.1,则threshold提高。即 Full GC 的时间太多,则增加 TenuringThreshold的值,让更少的对象进入老年代。

  而目前 credit.afp_fund_project.center 触发 YGC 频次大约在一秒一次,Full GC 触发频次在 2分钟一次,频繁的 YGC 导致 TT值不断减小。

TT 调整规则,详细可以看源码解读:https://github.com/unofficial-openjdk/openjdk/blob/jdk9/jdk9/hotspot/src/share/vm/gc/parallel/psAdaptiveSizePolicy.cpp

3.1.2.2 EC / SC 比值不合理

  93184.0(EC) / 3584.0(S0C) = 26,JVM参数 -XX:SurvivorRatio=8 默认值就是 8,为啥这里实际比值已经达到了26?这是因为 Parallel 回收器 在开启了 -XX:+UseAdaptiveSizePolicy(自适应),如果 JVM 参数没有显示设置 -XX:SurvivorRatio=8 ,会动态调整 EC 和 SC 的比值的。调整规则:如果 YGCOld GC 时间超过了目标停顿时间,则会触发调整 Eden 区大小逻辑。

  EC / SC 比值过小容易触发老年代担保机制。举例:比如触发 YGC,SOC 区可用容量为 3584.0K,但目前存活的对象有3590.0K,那么多出来的 6K 对象,则会直接进入到老年代,从而导致老年代不断增大触发 Full GC

3.1.2.3 分析 YGC 情况

  执行如下命令,监视 java 堆 gc 情况

jstat -gc  14036 20

  抽取了2次在触发 YGC 前后情况

S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
1024.0 2048.0  0.0   1652.7 94720.0  90042.2   201728.0   169753.6  133268.0 126021.6 16276.0 14865.1  48718  342.669  425    80.804  423.473
1024.0 3072.0 1020.1  0.0   92160.0   3775.8   201728.0   171555.3  133268.0 126021.6 16276.0 14865.1  48719  342.677  425    80.804  423.481

3072.0 1536.0  0.0   1332.4 106496.0 99209.2   208896.0   164562.2  133268.0 126017.9 16276.0 14864.5  49027  345.190  432    82.151  427.341
2048.0 3072.0 1536.3  0.0   103424.0  3007.9   208896.0   165305.2  133268.0 126017.9 16276.0 14864.5  49027  345.196  432    82.151  427.346

  第一次 YGC 分析,OU之后 - OU之前 = 171555.3 - 169753.6 = 1801.7,也就是说有 1801.7k 存活对象在这次 YGC 后进入了老年代;再来看 SO 区,SOC = 1024.0K,SOU = 1020.1K,也就是说 SO区 容量只有 1024.0K,经过这次 YGC 后,SO 区存活的对象占用 1020.1K。从这里可以看出,这里极大可能触发了老年代担保机制,朝生夕灭的对象由于 S区不足逃逸到老年代

  再来分析第二次 YGC,OU之后 - OU之前 = 165305.2 - 164562.2 = 743,也就是说有 743k 存活对象在这次 YGC 后进入了老年代;再来看 SO 区,SOC = 2048.0K,SOU = 1536.3K,从这里可以看出 YGC 之后,SO区还是有 500K 的容量,那 743k 存活对象晋升到老年代极大可能是由于触发条件:年龄 = 2 > TT =1,朝生夕灭的对象由于 TT 值太小从而逃逸到老年代

3.2 问题二:扩容和收缩机制

  这跟所选的垃圾回收器有关。Parallel 回收器 注重吞吐量最大停顿时间,它的缩、扩容机制如下。

  • 收缩Parallel 收集器 在没有达到最大暂定时间目标,则每次只会缩小一个代内存容量,如果2个代的的暂停时间都高于目标,那么暂停时间较大的代的容量将首先缩小

最大暂停时间:-XX:MaxGCPauseMillis=100,如果开启了 -XX:+UseAdaptiveSizePolicy:自适应,MaxGCPauseMillis 参数的设置就没意义了,JVM在内部会自动调整最大暂停时间目标)

  • 扩容:如果吞吐量目标(jinfo 命令查看-XX:GCTimeRatio=99)没有达到,那么两个代的容量都将增加

官方引用链接:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html#parallel_collector_ergonomics

执行如下命令,查看 Xms 初始化堆内存大小的值

jinfo -flag InitialHeapSize 14036
// InitialHeapSize = Xms

在这里插入图片描述

  可以看出初始堆内存大小 = 250m,同样初始的E区内存也很小,因为 project 项目请求量大,过小的E区,会造成频繁 YGC,之前说过 YGC 频次大约在一秒一次,一次耗时在6~10ms,虽然 YGC 很频繁,但是每次耗时很低,导致吞吐量目标还是达到了,没有扩容,这种情况建议使用 Parallel 收集器 建议把-Xms-Xmx设置为相同的数值。

GCTimeRatio = 应用程序执行时间 / 垃圾回收耗时 > 99

四、解决方案

  经过上面问题一、问题二的分析,我们基本可以把问题定在这几个方面

  1. 年龄阀值(TT)太小了
  2. S区太小了
  3. 堆内存达不到最大 512M

4.1 问题一解决方案:

  年龄阀值(TT)太小了。Parallel 收集器 的动态年龄判断机制不好直接控制,只能从其他方面合理的控制 YGCFull GC 频次。

4.2 问题二解决方案:

  JVM参数显示设置 -XX:SurvivorRatio=8 或者关闭 -XX:-UseAdaptiveSizePolicy(自适应)

4.3 问题三解决方案:

  JVM参数 -Xms:512-Xmx:512 设置为相同的数值

4.4 额外补充:

  Parallel 收集器 有个天然的弊端,就是它没有单独回收老年代的机制。如果老年代占用内存越来越大,一定会触发 Full GC。如果触发频次不高,堆内存不大,耗时 = 200ms 情况都还好;但如果堆内存比较大 = 8G(笔者之前遇到过),一天一次 Full GC,但每次耗时都在 2~5s,这是业务不能容忍的。

  前面分析了都是一些不正常的情况导致垃圾对象逃逸到老年代,但如果正常情况下,非正常业务情况呢?

  1. 见过不少开发者都在本地设置缓存,缓存30分钟使用替换引用方式更新一次,那么在引用替换之前,这部分缓存对象经过最大年龄阀值(TT=15)进入老年代,替换引用之后,这部分对象都成了垃圾对象,这种情况下是会造成老年代越来越大的
  2. 某个接口大量被请求、或者大数据导出,如果触发 YGC,因为应用程序还在执行业务逻辑,那么会有大量的存活对象超过 S区容量从而逃逸到老年代。

4.5 采用 CMS 垃圾回收器

  综上所诉,还是建议使用 CMS 垃圾回收器 ,CMS 有单独回收老年代机制,相比 G1,能减少额外内存消耗。

五、dump 内存文件

  本次问题分析,并没有 dump 内存文件分析,没那个必要。Full GC 情况下服务还是在跑的,不能直接 dump,可能会造成大量接口超时等情况。

本章节后续补充

4.1 dump 方式一

4.2 dump 方式二

六、分析 dump 文件

本章节后续补充

七、调整 JVM 参数

7.1 新增参数

  • -XX:+UseConcMarkSweepGC:开启CMS垃圾回收器
  • -XX:+UseParNewGC:开启ParNew垃圾回收器
  • -XX:+SafepointTimeout:开启安全点停顿超时时间设置
  • -XX:SafepointTimeoutDelay=1000:设置安全点停顿超时时间阈值
  • -XX:+PrintSafepointStatistics:打印在安全点停顿超时的线程信息
  • -XX:+PrintTenuringDistribution:JVM 在每次新生代GC时,打印出幸存区中对象的年龄分布(对 -XX:+UseParallelGC无效)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Parallel Scavenge GCParallel Old GC都是JVM中的垃圾回收器,两者都是基于并行的垃圾收集算法。 Parallel Scavenge GC是专门为了提高应用程序的吞吐量而设计的。它的主要特点是在GC期间,尽量利用CPU资源来并行地处理垃圾回收。Parallel Scavenge GC的主要策略是将堆内存划分为两个区域:一个是新生代,一个是老年代。在新生代区域中,Parallel Scavenge GC采用了复制算法,在垃圾回收时将存活的对象复制到另一个区域中,同时清空原来的区域,这样就达到了快速回收内存的目的。在老年代区域中,Parallel Scavenge GC采用了标记-整理算法,在垃圾回收时将存活的对象整理到一端,然后清理掉没有被标记的对象。 Parallel Old GC则是Parallel Scavenge GC的补充,它主要是为了解决老年代的垃圾回收问题。Parallel Old GC采用了标记-整理算法,在垃圾回收时将存活的对象整理到一端,然后清理掉没有被标记的对象。Parallel Old GC使用多个线程并行地进行垃圾回收,以达到快速回收内存的目的。Parallel Old GC还支持增量模式,在垃圾回收时可以与应用程序并发执行,减少了GC对应用程序的影响。 总的来说,Parallel Scavenge GCParallel Old GC都是基于并行的垃圾收集算法,它们的设计都是为了提高应用程序的吞吐量。Parallel Scavenge GC主要用于新生代的垃圾回收,而Parallel Old GC则主要用于老年代的垃圾回收。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值