很意外,一个小优化竟让接口耗时下降60%,CPU负载降低30%

大家好,我是五阳。

GC 话题始终霸占面试必问排行榜,很多人对 GC 原理了然于胸,但是苦于没有实践经验,因此本篇文章将分享我的GC 优化实践。一个很小的优化,产生了非常好的效果。

现在五阳将优化过程给大家汇报一下。


一、背景

我所负责的 A 服务每天的凌晨会定时执行一个批量任务,每天执行时都会触发 GC 频率告警,偶尔单机 CPU 负载超过 60%时,会触发 CPU 高负载告警。

曾经有考虑过通过单机限流器,限制任务执行速率,从而降低机器负载。然而因为业务上希望定时任务尽快执行完,所以优化方向就放在了如何降低 CPU 负载,如何降低 GC 频率。

1.1 配置和负载

  • 版本:java8
  • GC 回收器:ParNew + CMS
  • 硬件:8 核 16G 内存,Centos6.8
  • 高峰期CPU 平均负载(分钟)超过 50%(每个公司计算口径可能不同。我司的历史经验超过 70%后,接口性能将会快速恶化)

1.2 优化前的 GC情况

不容乐观。

  • 高峰期 Young GC频率 70次/min,单次 ygc 平均时间 125ms;
  • 高峰期 Full GC频率 每 3 分钟 1 次;单次 fgc 平均时间 610ms。

1.3 GC 参数和 JVM 配置

参数配置 说明
-Xmx6g -Xms6g 堆内存大小为6G
-XX:NewRatio=4 老年代的大小是新生代的 4 倍,即老年代占4.8G,新生代占1.2G
-XX:SurvivorRatio=8 Eden:From:To= 8:1:1,即Eden区占0.96G,两个Survivor区分别占0.12G
-XX:ParallelCMSThreads=4 设置 CMS 垃圾回收器使用的并行线程数为 4
XX:CMSInitiatingOccupancyFraction=72 设置老年代使用率达到 72% 时触发 CMS 垃圾回收。
-XX:+UseParNewGC 启用 ParNew 作为年轻代垃圾回收器
-XX:+UseConcMarkSweepGC 启用 CMS 垃圾回收器

二、问题分析

2.1 增加 GC打印参数

由于打印GC信息不足,无法分析问题。因此添加了 以下GC 打印参数,以提供更多的信息

js 复制代码
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-XX:+PrintCommandLineFlags 
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintReferenceGC

2.2 提前晋升现象

配置如上参数后,每次发生 younggc后,都会打印详细的 younggc 日志。通过分析 gc 日志,我发现日志中经常出现类似内容。 Desired survivor size 61054720 bytes, new threshold 2 (max 15)

new threshold是新的晋升阈值,是指对象在新生代经过 new threshold 轮 younggc后,就能晋升到老年代,这个值通过 MaxTenuringThreshold配置,默认值是 15,在原有理解中阈值是固定值 15,实际上这个值会动态调整。

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

Desired survivor 一般是 Survivor 区的一半。假设年龄 1至N 的对象大小,超过了 Desired size,那么下一次 GC 的晋升阈值就会调整为 N。举个例子,假设 age=1的对象为 80M,超过了 61M,那么下一次GC 的晋升阈值就是 1,所有超过 1 的对象都会晋升到老年代,无需等到年龄到 15。

如何分析 younggc 日志,可以参考我的另一篇文章。2024面试必问:系统频繁Full GC,你有哪些优化思路?第一步分析gc日志

2.3 老年代增长速度过快

为了印证是否发生提前晋升,我通过监控查看到在事发时间,老年代内存的涨幅和 Survivor的内存基本一致,看来新生代的对象确实提前晋升到老年代了。

grep 分析历次 GC 后的晋升阈值后,我发现绝大部分情况下,新生的对象无法在 15 次 GC后进入到老年代,基本上三次以后就会提前晋升到老年代...... 这解释了为什么会发生频繁的 FullGC。

假设每次提前晋升 100M 到老年代,每分钟超过 15 次 ygc,则每分钟将会有 1.5G 对象进入老年代。

因为频繁地提前晋升,老年代的增长速度极快。 在高峰期时,往往 2 至 3 分钟左右,老年代内存就会触达 72% 的阈值,从而发生 FullGC。

2.4 新生代内存不足

即便老年代配置 4.8G 的大内存,但频繁地发生提前晋升,老年代也很快被打满。这背后的根本原因在于 新生代的内存太小了。 新生代,总共 1.2G 大小,Survivor才 120M,这远远不够。

于是我们调整了内存分配。调整后如下

  • -Xmx10g -Xms10g -Xmn6g
  • -XX:SurvivorRatio=8
  1. 堆内存由 6G 增加到 10G
  2. 大部分堆内存(6G)分配给新生代。新生代内存从 1.2G 增加到 6G。
  3. Eden:From:To 的比例依然是 8:1:1
  4. Eden大小从 0.96 G 增加到 4.8 G。
  5. Survivor区由 120 M 增加到 600 M。

三、优化效果

虽然改动不大,但是优化效果十分显著。由于公司监控有水印,我无法截图取证,敬请谅解。

3.1 GC频率明显下降

  • 高峰期 ygc 70 次/min 降到了 12 次/min,下降幅度达83%(单机 500 QPS)
  • 高峰期 fgc 三分钟1 次 ,降到了 每天 1 次 Full GC。
  • younggc 和 fullgc 单次平均耗时保持不变。

3.2 CPU 负载降低 30%+

  • 优化之前高峰期 cpu 平均负载超过 50%;优化后降到了不足 30%,高峰期负载下降了 40%。
  • CPU负载每日平均值 由 29%,降到了 20%。日平均负载下降了 32%。

3.3 核心接口性能显著提升

核心接口耗时下降明显

  • 接口 A 高峰期 TPS 100/秒,tp999 由 200毫秒 降到了 150 毫秒, tp9999 由 400 毫秒降到了 300 毫秒,接口耗时下降超过 25%!
  • 接口 B 高峰期QPS 250/秒, tp999 由 190 毫秒降到了 120 毫秒, tp9999 由 450 毫秒下降到了 150 毫秒,接口耗时下降分别下降 37%和 67%!
  • 接口 B 低峰期降幅更加明显,tp999 由 80 毫秒降到了 10 毫秒,下降幅度接近 90%!

后来又适当微调了 JVM 内存分配比例,但是优化效果不明显。

四、总结

经过此次 GC 优化经历,我学到了如下经验

  • 要通过 GC 日志分析 GC 问题。
  • 调整JVM 内存,保证足够的新生代内存。
  • 优化 GC 可以降低接口耗时,提高接口可用性。
  • 优化 GC 可以有效降低机器 CPU 负载,提高硬件使用率。

反过来当接口性能差、cpu负载高的时候,不妨分析一下 GC ,看看有没有优化空间。

详细了解如何分析 younggc 日志,可以参考我的另一篇文章。2024面试必问:系统频繁Full GC,你有哪些优化思路?第一步分析gc日志

关注五阳~ 了解更多我在大厂的实际经历

相关推荐
阿望要努力上研究生2 小时前
若依项目搭建(黑马经验)
java·redis·node.js·maven·管理系统
一只脑洞君2 小时前
Kubernetes(K8s)的简介
java·容器·kubernetes
zygswo2 小时前
程序猿成长之路之设计模式篇——设计模式简介
java·设计模式
除了代码啥也不会3 小时前
springboot项目发送邮件
java·spring boot·spring
无敌の星仔4 小时前
一个月学会Java 第7天 字符串与键盘输入
java·开发语言·python
GGBondlctrl4 小时前
【JavaEE初阶】多线程案列之定时器的使用和内部原码模拟
java·开发语言·定时器·timer的使用·定时器代码模拟
多多*4 小时前
OJ在线评测系统 微服务高级 Gateway网关接口路由和聚合文档 引入knife4j库集中查看管理并且调试网关项目
java·运维·微服务·云原生·容器·架构·gateway
惜.己5 小时前
java中日期时间类的api
java·开发语言·intellij-idea·idea·intellij idea
橘子海全栈攻城狮6 小时前
【源码+文档+调试讲解】基于Android的固定资产借用管理平台
android·java·spring boot·后端·python·美食