前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >记录线上服务频繁full gc问题排查

记录线上服务频繁full gc问题排查

原创
作者头像
姆斯java实战分享
修改2023-07-23 23:47:13
7240
修改2023-07-23 23:47:13
举报

线上服务GC问题,是JAVAJAVA程序比较典型的问题,也是非常考验工程师的排查能力。能真正排查定位的人不多,要么原理没吃透、要么没有实战经验,看到此问题无从下手。

过去2个月,转换服务、基础配置服务出现多次和GC相关的线上问题。有FULL GC过于频繁的,有Young GC耗时过长,CPU飙升等,这些问题带来了影响是:GC过程中

程序卡顿、程序执行耗时长、进一步导致服务超时从而影响到转换功能、基础上下游调用链。影响用户体验

将以最近一次基础服务FULL GC频繁的线上案例作为引子,详细介绍排查过程、思路、方法论;

1、从最近一次基础服务FULL GC频繁的线上案例说起

2、排查gc思路、方法论、过程

3、GC的原理介绍

一、案例:

2023-1-11至12号收到了基础配置服务监控告警频繁FULL GC,结合提示信息,使用skywalking找到具体的服务监控信息,通过skywalking追踪可以看到16点之前平均半个小时FULL GC 2个,16点平均1分钟2点

基础服务那天平均半个小时1次full gc

(由于基础服务监控信息没有保留超过有效期数据清掉了,借用格式转换的图来说明指标)

二、排查gc思路、方法论、过程

由于1-4的步骤通常可以让运维去执行,目前公司生产则一律由运维管理,所以直接找运维dump堆内存文件即可

1、检查JVM配置

2、观察监控信息中老年代内存变化

3、通过jmap命令查看堆内存中的对象

通过命令 

jmap -histo 7276 | head -n20

图片是借助网图,因生产没权限只有运维来执行

上图中,按照对象所占内存大小排序,显示了存活对象的实例数、所占内存、类名。可以看到排名第一的是:int[],而且所占内存大小远远超过其他存活对象。至此,我们将怀疑目标锁定在了 int[] 。

4、通过jmp命令进一步dump堆内存文件进行分析

锁定 int[] 后,我们打算dump堆内存文件,通过可视化工具进一步跟踪对象的来源。考虑堆转储过程中会暂停程序,因此我们先从服务管理平台摘掉了此节点,然后通过以下命令dump堆内存:

jmap -dump:format=b,file=heap 7276

5、借助mat工具分析dump堆文件

OOM/频繁full gc:主要观察堆、元数据区、栈

分析快照文件思路或者方法论:

1、内存占用过大的对象是什么?

2、这个对象被谁引用了?

3、定位到具体的代码

还有一种可能就是第三方中间件---如:tomcat,比如每个请求过来的时候tomcat都会为每个请求生成线程并创建2个缓冲区,缓冲区默认大小就是10M,这个时候想象下如果每个请求都会创建2个缓冲区,当

并发量特别大点时候也就创建很多缓冲区很有可能导致堆内存不够用导致oom。如果出现tomcat问题导致,这个时候就需要对tomcat有一定对掌握才能去定位解决。

mat 使用:

mat可以单独安装也可以利用eclipse安装Memory Analyzer插件,由于我电脑是mac m1芯片只能借助eclipse

1、首先将dump文件下载到本地,然后导入到mat工具/eclipse。如下图:

选择文件导入

导入后选择分析选项:

Leak Supacts Report(大概意思就是分析oom以及full gc具体信息),一般选择这个就足够了

查看内存占用过大的指标:

导入后首页会分析大概占用内存比例

下面图片很明显:bytes占用了36%的内存,占比最高

点击MAT的Histogram来进行查询,一般是按照占用内存倒序进行排序的。

shallow heap-浅堆内存(其实就是这个对象实际占用的内存总量)

retained heap-深堆内存(表达:可释放内存,也可以理解目前没有回收占用的内存总量)

查看占有内存多大的对象被谁引用了

点击MAT的dominator_tree,用来分析对象的调用链

定位具体的代码

点击MAT的thread_overview,线程简介图,这个里面有方法的调用链

分3个步骤分析即可,

1、观察name、shallow heap retained heap指标,一般也是倒序排序,

name是代表那个java类

shallow heap-浅堆内存(其实就是这个对象实际占用的内存总量)

retained heap-深堆内存(表达:可释放内存,也可以理解目前没有回收占用的内存总量)

2、查看占比最大2个即可,打开java.lang.thread @xd3330000展示详情,从下往上查看

3、查看线程详情

选中线程邮件点击详情Thread Details查看具体发生的异常信息

解决方案

业务代码导致频繁full gc ,优化业务代码即可。发生full gc有很多种原因,结合自己程序问题解决即可

三、GC的运行原理介绍

上面整个案例的分析过程中,其实涉及到很多GC的原理知识,如果不懂得这些原理就着手处理,其实整个排查过程是很抓瞎的。

这里,我选择几个最核心的知识点,展开介绍下GC的运行原理,最后再给出一份实践指南。

堆内存结构

大家都知道: GC分为YGC和FGC,它们均发生在JVM的堆内存上。先来看下JDK8的堆内存结构:

可以看到,堆内存采用了分代结构,包括新生代和老年代。新生代又分为:Eden区,From Survivor区(简称S0),To Survivor区(简称S1区),三者的默认比例为8:1:1。另外,新生代和老年代的默认比例为1:2。

堆内存之所以采用分代结构,是考虑到绝大部分对象都是短生命周期的,这样不同生命周期的对象可放在不同的区域中,然后针对新生代和老年代采用不同的垃圾回收算法,从而使得GC效率最高。

YGC是什么时候触发的?

大多数情况下,对象直接在年轻代中的Eden区进行分配,如果Eden区域没有足够的空间,那么就会触发YGC(Minor GC),YGC处理的区域只有新生代。因为大部分对象在短时间内都是可收回掉的,因此YGC后只有极少数的对象能存活下来,而被移动到S0区(采用的是复制算法)。

当触发下一次YGC时,会将Eden区和S0区的存活对象移动到S1区,同时清空Eden区和S0区。当再次触发YGC时,这时候处理的区域就变成了Eden区和S1区(即S0和S1进行角色交换)。每经过一次YGC,存活对象的年龄就会加1。

FGC又是什么时候触发的?

下面4种情况,对象会进入到老年代中:

  • YGC时,To Survivor区不足以存放存活的对象,对象会直接进入到老年代。
  • 经过多次YGC后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。
  • 动态年龄判定规则,To Survivor区中相同年龄的对象,如果其大小之和占到了 To Survivor区一半以上的空间,那么大于此年龄的对象会直接进入老年代,而不需要达到默认的分代年龄。
  • 大对象:由-XX:PretenureSizeThreshold启动参数控制,若对象大小大于此值,就会绕过新生代, 直接在老年代中分配。

当晋升到老年代的对象大于了老年代的剩余空间时,就会触发FGC(Major GC),FGC处理的区域同时包括新生代和老年代。除此之外,还有以下4种情况也会触发FGC:

  • 老年代的内存使用率达到了一定阈值(可通过参数调整),直接触发FGC。
  • 空间分配担保:在YGC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果小于,说明YGC是不安全的,则会查看参数 HandlePromotionFailure 是否被设置成了允许担保失败,如果不允许则直接触发Full GC;如果允许,那么会进一步检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于也会触发 Full GC。
  • Metaspace(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时,也会触发FGC。
  • System.gc() 或者Runtime.gc() 被显式调用时,触发FGC。

在什么情况下,GC会对程序产生影响?

不管YGC还是FGC,都会造成一定程度的程序卡顿(即Stop The World问题:GC线程开始工作,其他工作线程被挂起),即使采用ParNew、CMS或者G1这些更先进的垃圾回收算法,也只是在减少卡顿时间,而并不能完全消除卡顿。

那到底什么情况下,GC会对程序产生影响呢?

根据严重程度从高到底,我认为包括以下4种情况:

  • FGC过于频繁

:FGC通常是比较慢的,少则几百毫秒,多则几秒,正常情况FGC每隔几个小时甚至几天才执行一次,对系统的影响还能接受。但是,一旦出现FGC频繁(比如几十分钟就会执行一次),这种肯定是存在问题的,它会导致工作线程频繁被停止,让系统看起来一直有卡顿现象,也会使得程序的整体性能变差。

  • YGC耗时过长

:一般来说,YGC的总耗时在几十或者上百毫秒是比较正常的,虽然会引起系统卡顿几毫秒或者几十毫秒,这种情况几乎对用户无感知,对程序的影响可以忽略不计。但是如果YGC耗时达到了1秒甚至几秒(都快赶上FGC的耗时了),那卡顿时间就会增大,加上YGC本身比较频繁,就会导致比较多的服务超时问题。

  • FGC耗时过长

:FGC耗时增加,卡顿时间也会随之增加,尤其对于高并发服务,可能导致FGC期间比较多的超时问题,可用性降低,这种也需要关注。

  • YGC过于频繁

:即使YGC不会引起服务超时,但是YGC过于频繁也会降低服务的整体性能,对于高并发服务也是需要关注的。

其中,「FGC过于频繁」和「YGC耗时过长」,这两种情况属于比较典型的GC问题,大概率会对程序的服务质量产生影响。剩余两种情况的严重程度低一些,但是对于高并发或者高可用的程序也需要关注。

排查FGC问题的实践指南

通过上面的案例分析以及理论介绍,再总结下FGC问题的排查思路,作为一份实践指南供大家参考。

清楚从程序角度,有哪些原因导致FGC?

  • 大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。
  • 内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发FGC,最后导致OOM.
  • 程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发FGC. (即本文中的案例)
  • 程序BUG导致动态生成了很多新类,使得 Metaspace 不断被占用,先引发FGC,最后导致OOM.
  • 代码中显式调用了gc方法,包括自己的代码甚至框架中的代码。
  • JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。

清楚排查问题时能使用哪些工具

公司的监控系统:大部分公司都会有,可全方位监控JVM的各项指标。

JDK的自带工具,包括jmap、jstat等常用命令:

  • # 查看堆内存各区域的使用率以及GC情况
  • jstat -gcutil -h20 pid 1000
  • # 查看堆内存中的存活对象,并按空间排序
  • jmap -histo pid | head -n20
  • # dump堆内存文件
  • jmap -dump:format=b,file=heap pid

可视化的堆内存分析工具:JVisualVM、MAT等

排查指南

  1. 查看监控,以了解出现问题的时间点以及当前FGC的频率(可对比正常情况看频率是否正常)
  2. 了解该时间点之前有没有程序上线、基础组件升级等情况。
  3. 了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理。
  4. 再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。
  5. 针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo 命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象。
  6. 通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、案例:
  • 二、排查gc思路、方法论、过程
    • 1、检查JVM配置
      • 2、观察监控信息中老年代内存变化
        • 3、通过jmap命令查看堆内存中的对象
        • 三、GC的运行原理介绍
        相关产品与服务
        消息队列 TDMQ
        消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档