欢迎大家点赞,收藏,评论,转发,你们的支持是我最大的写作动力
作者: 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频繁问题,我通常遵循「观察→分析→假设→验证」四步法:
- 查看GC日志确认GC类型和频率
- 分析内存使用趋势判断是否存在内存泄漏
- 检查JVM参数配置是否合理
- 通过线程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工作流程分为四个阶段:
- 初始标记(STW):标记GC Roots直接关联的对象
- 并发标记:并发遍历对象图,耗时较长但不阻塞用户线程
- 重新标记(STW):修正并发标记期间因用户线程操作导致的标记变动
- 并发清除:并发清除标记的对象,回收内存空间
2.3 FULL GC频繁的常见原因
结合理论知识和实践经验,FULL GC频繁通常有以下几类原因:
- 内存泄漏:对象无法被回收,持续占用老年代空间
- 老年代空间不足:新生代对象晋升过快,老年代无法容纳
- CMS配置不当:如触发阈值设置过高、内存碎片严重
- 大对象直接进入老年代:超过-XX:PretenureSizeThreshold的对象直接分配到老年代
- 元空间溢出:类加载过多导致元空间不足触发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-1200ms | 200-400ms | 降低60% |
Minor GC频率 | 20-30次/分钟 | 10次/分钟 | 降低60% |
老年代使用率 | 90%-95% | 40%-60% | 降低45% |
接口响应时间 | 50-3000ms | 100-300ms | 降低97% |
2天后的的JVM运行情况 jstat -gcutil <pid> 1000
FULL GC明显降低,Minor GC频率也得到了优化,老年代使用率也得到了明显改善,接口响应时间也得到了显著提升。客服现在也不抱怨了。虽然只是一个折中的办法,但是确实是有效的。
看后续服务器能否升级到32G,到时候直接上G1垃圾收集器看看效果如何。
不是兔哥不敢改,历史屎山代码真的不好改,先把问题解决了,后续再优化,这也不失为一种好的解决方式。
六、JVM调优最佳实践与避坑指南
6.1 调优方法论
JVM调优不是玄学,而是科学的工程实践,建议遵循以下步骤:
- 确立目标:明确性能指标(如响应时间、吞吐量、GC频率)
- 基准测试:建立性能基准线,作为调优对比依据
- 监控分析:全面收集GC日志、内存使用、线程状态等数据
- 针对性优化:根据数据分析结果,制定具体优化方案
- 验证效果:对比优化前后指标,确认是否达到目标
- 持续监控:性能优化是长期过程,需持续监控系统状态
6.2 常见调优误区
- 盲目增大堆内存:堆内存并非越大越好,过大的堆会增加GC停顿时间
- 过度追求低GC频率:适当的Minor GC是正常现象,不必追求"零GC"
- 忽视内存泄漏:调优参数无法解决内存泄漏问题,需从代码层面修复
- 照搬他人参数:不同应用场景需要不同调优策略,切勿直接复制粘贴
- 忽视GC日志:GC日志是问题诊断的重要依据,必须开启并定期分析
6.3 进阶调优方向
- 收集器选择:JDK11+可考虑ZGC,JDK17+推荐使用Shenandoah,获得更低延迟
- 内存分配优化:使用TLAB (Thread Local Allocation Buffer) 减少锁竞争
- JVM参数精细化:如调整-XX:CMSTriggerRatio、-XX:CMSMaxAbortablePrecleanTime等参数
- 代码级优化:减少对象创建、避免大对象、合理使用缓存
- 监控体系建设:集成Prometheus+Grafana+AlertManager,构建完善的监控告警体系
七、总结与思考
本次JVM调优实战从FULL GC频繁问题出发,通过参数优化和代码修复,使系统性能得到显著提升。关键经验总结如下:
- 问题定位是核心:调优前必须通过日志和工具准确找到问题根源,避免盲目调参
- 参数调优与代码优化结合:JVM参数是"治标",代码优化才是"治本",两者需结合
- 监控体系不可少:建立完善的监控体系,及时发现和解决性能问题
- 持续优化是常态:随着业务发展,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