你好,欢迎进入江苏优软数字科技有限公司官网!

诚信、勤奋、创新、卓越

友好定价、专业客服支持、正版软件一站式服务提供

13262879759

工作日:9:00-22:00

codejock 162 JVM垃圾收集实战:从问题发现到解决,系统性排查与调优案例解析

发布时间:2025-11-06

浏览次数:0

前言

将通过一个真实生产环境的案例,此案例包含从问题发现起,历经分析诊断,直至最终解决的完整过程,来详细展示怎样系统性地排查以及解决JVM垃圾收集问题,它对于理解JVM调优实战有着重要的参考价值。

系统背景

我们的服务是一个高并发的微服务应用,技术栈如下:

指明:它身为应用监控的门面库,具备支持诸多监控系统的功能,其中涵盖了:Atlas,还有其他多个未明确指出的系统,诸如Humio,以及JMX,另外还有New Relic,以及其他多个未明确指出的系统等等。

详细信息请参考:

https://.io/docs

问题现象

问题描述

于日常监控期间,我们察觉到,有一个服务节点,呈现出了极为严重的GC性能方面的问题 。

GC暂停时间监控图

业务影响

这种GC暂停时间严重影响了业务运行:

最大GC暂停时间 < 200ms

平均暂停时间 < 100ms

问题排查过程

第一步:系统资源使用分析

CPU负载分析

首先检查CPU使用情况,监控数据如下:

CPU负载监控图

观察结果:

系统负载:4.92

CPU使用率:约7%

要点提示:此监控图里实际上蕴含着关键线索,也就是CPU核心数跟GC线程数存在不匹配的状况,然而当时并没有察觉到有什么异常之处。

GC 内存使用情况

然后我们排查了这段时间的内存使用情况:

从那幅图之内呢,可以瞅见,大概在09:25前后的时候,使用量急剧地大幅下跌,真确切切是已然发生了的 。

但是,在09:20的前后时刻,使用量于老年代空间以内呈现出缓慢上升的态势,并非处于下降状态,这也就表明,引发最大暂停时间的那个关键节点并未出现 。

当然,这些结论是通过事后进行复盘分析才得出来的。当时,对于监控所反馈的那些信息,并不是特别信任,怀疑是触发了才导致长时间的 GC 暂停。

为什么会存在怀疑呀,是由于,这个监控系统,它默认是每隔10s就上报一回数据呢。存在这样一种可能性,即在这10s的时间范围里面,会发生某些事情,然而却被遗漏未报了(当然啦,这是绝对不可能出现的情况,要是上报时遭遇失败,那么会于日志系统当中打印出相关的错误信息)。

再来剖析上面那个图,能够瞧见老年代所对应的内存池是“”,经由先前的学习,我们晓得,ps指代的乃是垃圾收集器。

第三步:JVM配置分析

启动参数检查

检查JVM启动参数配置:

-Xmx4g -Xms4g

配置分析:

初步问题假设

怀疑点:可能是问题根源

第一次优化尝试:切换到G1GC

优化策略选择

选择G1垃圾收集器的理由:

稳定性:在JDK 8的新版本中,G1已经相当稳定

性能表现:具有良好的延迟控制能力

适用场景:更适合低延迟要求的应用

配置过程

初次配置(失败)

# 参数配置错误,导致启动失败
给予-Xmx4g的配置,给予-Xms4g的设置,启用-XX:+UseG1GC,设定-XX:MaxGCPauseMills为50ms 。

错误分析:

修正后配置

减 Xmx4g,减 Xms4g,加 XX 冒号 useG1GC,加 XX 冒号 MaxGCPauseMillis 赋值为 50 。

初步效果验证

服务启动成功,通过健康检测切换到新节点后,监控显示:

G1GC初期效果监控图

效果评估:

但是,问题远未结束.....

“彩蛋”惊喜

当时间走过了一段历程之后,我们察觉到了在下方呈现出来的这样一个惊喜,当然也有可能是惊吓,具体情形如同接下来所展示的图片那样:

中奖了,运行一段时间后,最大 GC 暂停时间达到了 。

情况似乎更恶劣了。

继续观察,发现不是个别现象:

脑子是乱的,感觉好似是数据计差错了,像是将十秒之内的停顿时段统统累计合起来了。

注册 GC 事件监听

于是想出了一个办法,借助 JMX 来注册 GC 事件监听这一行为,将与之相关的信息径直打印出来。

关键代码如下所示:

// 每个内存池都注册监听
对于 (垃圾收集器管理接口的实例对象 mbean )然而。
管理工厂获取垃圾收集器MXBean,对此进行操作,操作步骤为对此集合进行判断,此集合中存在元素时,获取该元素中全部的垃圾收集器MXBean,以该集合为参数,调用管理工厂获取垃圾收集器MXBean,。
要是并非(管理实体 bean 属于通知发射器的实例这种情况),那这样子 ,。
        continue; // 假如不支持监听...
    }
最终,通知发射器 创建物 ,它被 以通知发射器 的形式 当成可管理Bean类型 进行转换。  ,  。
    // 添加监听
最后,通知监听器,监听器是通过获取全新的监听器,该全新监听器是依据管理对象来获取的 。
emitter,添加通知监听者,监听者为listener,无特定条件,无特定范围,以此方式进行 。
}

以这般方式起见,于程序之内,我们能够去监听 GC 事件,接着把相关信息予以汇总,或者输出至日志当中。

再启动一次,运行一段时间后,看到下面这样的日志信息:

{
"duration":1869,
"maxPauseMillis":1869,
"promotedBytes":"139MB",
“gcCause”的值为“G1清空暂停”,此时是什么情况呢,是在进行怎样一个过程呢,是在相关的操作里。
"collectionTime":27281,
"gcAction":"end of minor GC",
"afterUsage":
 {
"G1 Old Gen":"1745MB",
"Code Cache":"53MB",
"G1 Survivor Space":"254MB",
"压缩类空间":"9兆字节" ,所呈现的这个具体数值,是经过特定计算方式得出的,也是该特定主题下。
"Metaspace":"81MB",
"G1 Eden Space":"0"
 },
"gcId":326,

codejock 162_JVM垃圾收集问题排查_JVM调优实战案例

"collectionCount":326, “gcName”为,“G1 Young Generation” 。 "type":"jvm.gc.pause" }

情况确实有点不妙。

这次确凿无疑了,并非如此,而是年轻一代的垃圾回收机制,并且暂停下来的时长达到了这般程度。完全没有任何缘由可讲,我觉得这样的情形不合情理,而且去观察中央处理器的使用量也并不高。

搜罗了好多资料,想要去证实这个,并非是暂停时间codejock 162,仅仅是GC事件的结束时刻减掉其开启时刻 。

打印 GC 日志

假设这些方式缺乏可靠性,那么便唯有施展我们的最终方式:打印 GC 日志。

修改启动参数如下:

-将最大堆内存设置为4g,-把初始堆内存设置为4g,-启用G1垃圾收集器,-设置最大垃圾收集停顿时间为50毫秒 。
-将-Xloggc设置为gc.log,-XX开启+PrintGCDetails,-XX开启+PrintGCDateStamps 。

重新启动,希望这次能排查出问题的原因。

运行一段时间,又发现了超长的暂停时间。

分析 GC 日志

由于未涉及敏感数据,所以我们将GC日志下载至本地来开展分析。

定位到这次暂停时间超长的 GC 事件,关键的信息如下所示:

适用于 linux - amd64 JRE(1.8.0_162 - b12)的Java HotSpot(TM)64位服务器虚拟机(25.162 - b12),。
建成于2017年12月19日21时15分48秒,由“java_re”使用gcc 4.3.0 20080428(红帽4.2.0 - 8)完成 。
内存:4k 页面,物理内存144145548k(58207948k 可用),交换空间0k(0k 可用)。
CommandLine flags: 
减XX冒号初始堆大小等等于4294967296 ,减XX冒号最大垃圾收集停顿毫秒数等于50 ,减XX冒号最大堆大小等于4294967296 。
使用 -XX:+PrintGC,使用 -XX:+PrintGCDateStamps,使用 -XX:+PrintGCDetails,使用 -XX:+PrintGCTimeStamps 。
减XX加上使用压缩类指针,减低XX转而使用压缩对象指针,再减低XX进而启用G1垃圾收集器 。
2020年02月24日18时02分31秒853毫秒加上0800时区的时候,出现了2411.124这个数值后的记录,内容是,进行了垃圾回收停顿,具体是指G1疏散停顿,属于年轻代相关的,耗时1.8683418秒 。
[并行时间:壹仟捌佰陆拾壹点零毫秒,垃圾回收工人:四十八] 。
最小的[垃圾回收工作线程启动(毫秒数)是2411124.3,平均的为2411125.4,最大的为2411126.2,差值是1.9] 。
这里呈现的是扩展根扫描(毫秒)的数据结果,其最小值为0.0,平均值为0.3,最大值为2.7,差值为2.7,总和为16.8 。
其中,[更新RS(毫秒):最小值:0.0],[平均值:3.6],[最大值:6.8],[差值:6.8],[总和:172.9]。
经过处理的缓冲区,最小值,为零,平均值,是二点三,最大值,是八,差值,为八,总和,是一百一十一 。
扫描RS(毫秒),最小值为0.0,平均值为0.2,最大值为0.5,差值为0.5,总和为7.7 。
[代码根扫描(毫秒):最小值:0.0,平均值:0.0,最大值:0.1,差值:0.1,总和:0.3] ,被改写为:[代码根方面的扫描在以毫秒计啊:它的最小值是0.0 ,其平均值为0.0 ,最大的数值是0.1 ,。
针对对象进行复制操作时存在相关数据,其最小值为一千八百五十一点六,平均值为一千八百五十四点六,最大值为一千八百五十七点四,差值为五点八,总和为八万九千零二十点四 。
[终止(毫秒):最小值:0.0,平均值:0.0,最大值:0.0,差值:0.0,总和:0.6] ,这里的终止(毫秒)所涉及的最小值代表在相关操作过程中所出现的最小耗时为0.0毫秒 ,平均值指的是对一系列相关操作耗时。
[终止尝试: 最小值: 1, 平均值: 1.0, 最大值: 1, 差值: 0, 总和: 48] 可改写为:[有着这样的情况,终止尝试,其最小值是1,平均值为1.0,最大值为1,差值为0,总和是48。
有这样一组数据,其 GC Worker Other(基于毫秒)呈现如下情况:最小值为 0.0,平均值是 0.3,最大值为 0.7,差值为 0.6,总和为 14.7 。
[垃圾回收工作者总计(毫秒):最小值:1858.0,平均值:1859.0,最大值:1860.3,差值:2.3,总和:89233.3] ,其中,最小值指的是在垃圾回收工作者相关操作中所记录到的最小时间值为1858.0毫秒 ,平均值是多次操作时间的。
GC工作者结束(测量单位:毫秒),最小值为2412984.1,平均值为2412984.4,最大值为2412984.6,差值为0.5 。
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 1.5 ms]
   [Other: 5.8 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 1.7 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 1.1 ms]
      [Humongous Register: 0.1 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 2.3 ms]
[伊登:2024点0兆字节(2024点0兆字节)变成0点0字节(2048点0千字节),]。
    Survivors: 2048.0K->254.0M 
Heap,3633.6M(4096.0M)转变为1999.3M(4096.0M),]。
此组数据似乎是关于时间的记录,其中包含用户时间为1.67,系统用时为14.00,实际用时为1.87秒, 。

正面的 GC 事件是正常的,反面的 GC 事件同样是正常的,没能发现单独的或者并发标记周期,然而却找到了一些可疑的点。

GC 日志中揭露了几个关键信息,

这样分析之后,可以得出结论:

察觉到如此数量繁多的 GC 工作线程,我即刻萌生出警惕之感来究竟是为何呢,毕竟呀,那 heap 内存仅仅被设定为了 4GB 这么个数值呀。

一般依据CPU以及内存资源的配比情况来看,常见的比例大致是像4 核搭配4GB、4 核搭配8GB 这般的。

看看对应的 CPU 负载监控信息:

同运维同学进行沟通之后,知晓这个节点的配置被限定为 4 核 8GB 。

这样一来,GC 暂停时间过长的原因就定位到了:

处置措施为:

事实证明,打印 GC 日志确实是一个很有用的排查分析方法。

限制 GC 的并行线程数量

下面是新的启动参数配置:

 -Xmx4g -Xms4g
-XX 加上使用 G1GC 设置参数,-XX 设置最大垃圾收集停顿时间为 50 毫秒,-XX 设置并行垃圾收集线程数为 4 个 。
减 X 日志记录垃圾回收情况到 gc.log,添加 XX 打印垃圾回收详细信息,添加 XX 打印垃圾回收日期戳 。

在此地明确规定了 -XX:=4codejock 162,为何要如此进行配置呢,去瞧瞧关于这个参数的解释说明 。

-XX:ParallelGCThreads=n

设定 STW 阶段的并行线程数量,要是逻辑处理器小于或等于 8 个,那么默认值 n 就等同于逻辑处理器的数量。

要是逻辑处理器超出8个,那么默认值n大致等同于处理器数量的5/8 。该值在多数情形下算是个较为合理的值 。若是高配置的SPARC系统,那么默认值n大致等同于逻辑处理器数量的5/16 。

-XX:ConcGCThreads=n

设定并发标识的 GC 线程数量,那个通常的默认数值大致而言乃是某数的四分之一 。

正常情况下,并非需要去指定进行并发标记操作的垃圾回收线程数量,仅仅只需指定并行所使用的数量就行。

重新启动之后,看看 GC 暂停时间指标:

那个被红色箭头所指示出来的点,便是重启的时间点,能够发现,暂停的时间基本上全都处在50ms的范围之内。

后续的监控发现,这个参数确实解决了问题。

案例总结与思考

核心经验

历经这个完整的用于排查的案例,我们能够收获如下重要的经验:不存在量化的情况,便不会有改进。针对JVM问题的排查操作以及关于性能的调优举措,都必定是依据具体实际存在的监控数据来开展进行的。

使用的排查手段

本案例运用了以下关键技术手段:

GC性能维度评估

延迟维度:

吞吐量维度:

系统容量维度:

关键技术洞察

容器化环境的特殊挑战:

问题排查的系统性方法:

满足业务需求,要确保各项性能指标得以达成;保持在合理范围之内,资源占用需做到;如此这般,才达到了JVM调优的预期目标。

作者丨飘渺Jam

如有侵权请联系删除!

13262879759

微信二维码