JVM调优实战+arthas的调优之路

欢迎大家点赞,收藏,评论,转发,你们的支持是我最大的写作动力
作者: GO兔
博客: https://luckxgo.cn

一、线上告警:CPU100%,触发FULL GC频繁触发的紧急响应

这周上线了新功能,阿里云的监控系统的告警短信突然打破了宁静:(ECS)CPU使用率 的1分钟统计值连续满足5次 平均值>95%,导致接口响应时间从50ms飙升至5秒,部分请求超时失败。作为架构师,我深知CPU百分之一百意味着什么,第一个反应就是又是那个小兔崽子直接查询了大对象,直接搞。 若不及时解决,可能引发系统雪崩。

1.1 问题现象与影响范围

  • 直接表现:上服务区查看去,JVM每30-60秒执行一次FULL GC,每次GC耗时200-500ms
  • 业务影响:客服反馈,打开APP页面非常卡,部分请求超时
  • 系统指标:老年代内存占用快速达到95%以上,CPU使用率波动在99%

1.2 初步排查思路

面对FULL GC频繁问题,我通常遵循「观察→分析→假设→验证」四步法:

  1. 查看GC日志确认GC类型和频率
  2. 分析内存使用趋势判断是否存在内存泄漏
  3. 检查JVM参数配置是否合理
  4. 通过线程dump和内存快照定位问题根源

二、JVM内存模型与GC机制:理解问题本质

在深入调优之前,我们先回顾JVM内存模型和GC机制,这是解决FULL GC问题的理论基础。

2.1 内存区域划分

在这里插入图片描述

JVM内存主要分为五大区域,其中堆内存是GC的主要战场:

  • 新生代:存放新创建的对象,分为Eden区和两个Survivor区(From/To)
  • 老年代:存放存活时间较长的对象,当对象经历多次Minor GC后仍存活会进入老年代
  • 永久代/元空间:存储类信息、常量、静态变量等(JDK8后元空间取代永久代,使用本地内存)

2.2 GC收集器工作原理

兔哥公司目前使用的是JDK8, 所以使用的是ParNew+CMS组合收集器,这是JDK7/8中常用的并发收集方案:

  • ParNew:新生代并行收集器,多线程回收Eden区和Survivor区
  • CMS(Concurrent Mark Sweep):老年代并发标记清除收集器,以低延迟为目标

CMS工作流程分为四个阶段:

  1. 初始标记(STW):标记GC Roots直接关联的对象
  2. 并发标记:并发遍历对象图,耗时较长但不阻塞用户线程
  3. 重新标记(STW):修正并发标记期间因用户线程操作导致的标记变动
  4. 并发清除:并发清除标记的对象,回收内存空间

2.3 FULL GC频繁的常见原因

结合理论知识和实践经验,FULL GC频繁通常有以下几类原因:

  1. 内存泄漏:对象无法被回收,持续占用老年代空间
  2. 老年代空间不足:新生代对象晋升过快,老年代无法容纳
  3. CMS配置不当:如触发阈值设置过高、内存碎片严重
  4. 大对象直接进入老年代:超过-XX:PretenureSizeThreshold的对象直接分配到老年代
  5. 元空间溢出:类加载过多导致元空间不足触发Full GC

三、调优实战:参数解析与优化依据

服务器是4核16G配置,此配置可能略微极端,这里只是提供一种优化JVM参数的思路方式,也许可能有更好的方案。

直接上干货调优后JVM参数如下,我们逐一解析其作用和调优思路:

-Xms10000m -Xmx10000m -Xmn7g  
-XX:CMSFullGCsBeforeCompaction=3 -XX:+UseCMSCompactAtFullCollection 
-XX:CMSInitiatingOccupancyFraction=70 -XX:+ParallelRefProcEnabled 
-XX:+CMSScavengeBeforeRemark -XX:+CMSParallelRemarkEnabled  
-XX:+UseCMSInitiatingOccupancyOnly -XX:+UseParNewGC -XX:+UseConcMarkSweepGC    
-XX:SurvivorRatio=8 -XX:ParallelGCThreads=4 -XX:MaxTenuringThreshold=15  
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc-%t.log 
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M

3.1 堆内存配置:合理分配新生代与老年代

-Xms10000m -Xmx10000m -Xmn7g
  • -Xms/-Xmx:设置堆内存初始值和最大值均为10G,避免堆内存动态调整带来的性能开销
  • -Xmn7g:新生代大小设置为7G,老年代则为3G(总堆10G - 新生代7G)

调优依据

  • 项目为微服务架构,存在大量短期对象(如HTTP请求对象),适合较大新生代
  • 7:3的新生代与老年代比例,适用于对象存活率较低的业务场景
  • 固定堆大小避免JVM在内存紧张时频繁扩容,减少GC压力

3.2 CMS收集器核心优化

-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly
  • -XX:+UseParNewGC -XX:+UseConcMarkSweepGC:启用ParNew+CMS收集器组合
  • -XX:CMSInitiatingOccupancyFraction=70:老年代使用率达到70%时触发CMS回收
  • -XX:+UseCMSInitiatingOccupancyOnly:禁止JVM动态调整CMS触发阈值

调优依据

  • 默认CMS触发阈值通常在92%左右,高阈值容易导致并发模式失败(Concurrent Mode Failure)
  • 设置为70%给并发标记清理预留足够空间,降低Full GC风险
  • 固定触发阈值避免JVM根据历史GC情况动态调整带来的不确定性

3.3 内存碎片优化

-XX:CMSFullGCsBeforeCompaction=3 -XX:+UseCMSCompactAtFullCollection
  • -XX:+UseCMSCompactAtFullCollection:在FULL GC后执行内存压缩,消除碎片
  • -XX:CMSFullGCsBeforeCompaction=3:每3次FULL GC后执行一次内存压缩

调优依据

  • CMS是标记-清除算法,会产生内存碎片,导致大对象无法分配连续空间而触发FULL GC
  • 内存压缩需要STW(Stop The World),频繁压缩会增加延迟,因此设置为3次FULL GC后执行一次
  • 平衡内存碎片和GC停顿时间,适合对延迟敏感但能容忍偶尔较长停顿的业务

3.4 降低CMS停顿时间

-XX:+ParallelRefProcEnabled -XX:+CMSScavengeBeforeRemark -XX:+CMSParallelRemarkEnabled
  • -XX:+CMSParallelRemarkEnabled:并行执行重新标记阶段,缩短STW时间
  • -XX:+CMSScavengeBeforeRemark:在重新标记前先执行一次Minor GC,减少老年代引用的对象数量
  • -XX:+ParallelRefProcEnabled:并行处理Reference对象(如WeakReference)

调优依据

  • 重新标记阶段是CMS中较长的STW阶段,并行化可显著降低停顿时间
  • 提前执行Minor GC能减少老年代对象引用的新生代对象数量,降低重新标记工作量

3.5 新生代优化

-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
  • -XX:SurvivorRatio=8:Eden区与Survivor区比例为8:1:1(Eden:S0:S1=8:1:1)
  • -XX:MaxTenuringThreshold=15:对象最大晋升年龄为15,即对象在Survivor区经历15次Minor GC后进入老年代

调优依据

  • 8:1:1是新生代的默认比例,适合大多数场景,Eden区足够大减少Minor GC频率
  • 较高的MaxTenuringThreshold(15)允许短期存活对象在新生代多停留一段时间,降低老年代压力
  • 对于生命周期较短的请求对象,可在新生代被回收,避免进入老年代

3.6 GC日志配置

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc-%t.log 
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M
  • -Xloggc:gc-%t.log:指定GC日志文件路径,%t会替换为时间戳
  • -XX:+UseGCLogFileRotation:启用日志轮转,避免单个日志文件过大
  • -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M:最多保留10个日志文件,每个10M

调优依据

  • 详细的GC日志是问题诊断的基础,必须开启
  • 日志轮转避免磁盘空间被占满,同时保留足够的历史数据
  • 时间戳命名便于按时间顺序分析GC趋势

四、上神器!arthas 分析线上问题

arthas 兔哥推荐肯定好!

直接java -jar arthas-boot.jar,打开神器,选择好对应的进程,直接进入页面
在这里插入图片描述

4.1 查看JVM使用情况

直接输入

dashboard

好家伙直接出来了,很多关键信息,有点东西,但不多
在这里插入图片描述

赶紧上科技与狠活

ctrl+c退出后,咱再输入一个thread n -4 直接打印当前最忙的前 N 个线程并打印堆栈
在这里插入图片描述

好家伙,直接把线程信息都给我列出来了,直接找到对应业务代码,看下哪个小崽子写出来的代码

4.1 查看JVM使用情况

再输入一个heapdump,直接把堆栈信息保存下来,heap.hprof下载来慢慢分析,打开我们的
在这里插入图片描述

下载地址:https://visualvm.github.io/
直接加载堆栈信息文件heap.hprof

直接输入项目的包名前缀,直接过滤出我们的项目,然后点击分析,直接看到我们的代码
在这里插入图片描述

经过一顿分析,发现了问题所在

就是查询太多的对象,导致内存占用过高,历史项目过于复杂,后续再优化业务代码吧,先直接调整JVM参数,降低内存占用,再进行业务代码优化

五、调优效果验证:从指标到业务

5.1 GC指标对比

指标调优前调优后优化幅度
FULL GC频率10-15次/小时1-2次/天降低99%
FULL GC平均耗时800-1200ms200-400ms降低60%
Minor GC频率20-30次/分钟10次/分钟降低60%
老年代使用率90%-95%40%-60%降低45%
接口响应时间50-3000ms100-300ms降低97%

2天后的的JVM运行情况 jstat -gcutil <pid> 1000
在这里插入图片描述

FULL GC明显降低,Minor GC频率也得到了优化,老年代使用率也得到了明显改善,接口响应时间也得到了显著提升。客服现在也不抱怨了。虽然只是一个折中的办法,但是确实是有效的。

看后续服务器能否升级到32G,到时候直接上G1垃圾收集器看看效果如何。

不是兔哥不敢改,历史屎山代码真的不好改,先把问题解决了,后续再优化,这也不失为一种好的解决方式。

六、JVM调优最佳实践与避坑指南

6.1 调优方法论

JVM调优不是玄学,而是科学的工程实践,建议遵循以下步骤:

  1. 确立目标:明确性能指标(如响应时间、吞吐量、GC频率)
  2. 基准测试:建立性能基准线,作为调优对比依据
  3. 监控分析:全面收集GC日志、内存使用、线程状态等数据
  4. 针对性优化:根据数据分析结果,制定具体优化方案
  5. 验证效果:对比优化前后指标,确认是否达到目标
  6. 持续监控:性能优化是长期过程,需持续监控系统状态

6.2 常见调优误区

  1. 盲目增大堆内存:堆内存并非越大越好,过大的堆会增加GC停顿时间
  2. 过度追求低GC频率:适当的Minor GC是正常现象,不必追求"零GC"
  3. 忽视内存泄漏:调优参数无法解决内存泄漏问题,需从代码层面修复
  4. 照搬他人参数:不同应用场景需要不同调优策略,切勿直接复制粘贴
  5. 忽视GC日志:GC日志是问题诊断的重要依据,必须开启并定期分析

6.3 进阶调优方向

  1. 收集器选择:JDK11+可考虑ZGC,JDK17+推荐使用Shenandoah,获得更低延迟
  2. 内存分配优化:使用TLAB (Thread Local Allocation Buffer) 减少锁竞争
  3. JVM参数精细化:如调整-XX:CMSTriggerRatio、-XX:CMSMaxAbortablePrecleanTime等参数
  4. 代码级优化:减少对象创建、避免大对象、合理使用缓存
  5. 监控体系建设:集成Prometheus+Grafana+AlertManager,构建完善的监控告警体系

七、总结与思考

本次JVM调优实战从FULL GC频繁问题出发,通过参数优化和代码修复,使系统性能得到显著提升。关键经验总结如下:

  1. 问题定位是核心:调优前必须通过日志和工具准确找到问题根源,避免盲目调参
  2. 参数调优与代码优化结合:JVM参数是"治标",代码优化才是"治本",两者需结合
  3. 监控体系不可少:建立完善的监控体系,及时发现和解决性能问题
  4. 持续优化是常态:随着业务发展,JVM调优不是一劳永逸的,需要持续关注和调整

最后,JVM调优没有银弹,需要结合具体业务场景,不断实践和总结。希望本文的经验能帮助你解决类似的性能问题,构建更稳定高效的Java应用。


附录:JVM调优工具清单

  • GC日志分析:GCViewer、GCEasy、HPjmeter
  • 内存分析:MAT(Memory Analyzer Tool)、JProfiler
  • 性能监控:VisualVM、JConsole、Prometheus+Grafana
  • 命令行工具:jps、jstat、jmap、jstack、jinfo

参考资料

  • Oracle JVM官方文档
  • 《Java性能权威指南》(Java Performance: The Definitive Guide)
  • 《深入理解Java虚拟机》(周志明著)

关注兔哥,兔哥编写肯定好
作者: GO兔
博客: https://luckxgo.cn

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GO兔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值