来源:https://zhenbianshu.github.io/java
最近对负责的项目进行了一次性能优化,其中包括对 JVM 参数的调整,算是进行了一次简单的 JVM 调优,JVM 参数调整以后,服务的总体性能有 15% 左右的提高,还算不错。git
先介绍一下项目的基本状况:github
项目是一个高 QPS 压力的 web 服务,单机 QPS 一直维持在 1.5K 以上,因为旧机器的”拖累”,配置的堆大小是 8G,其中 young 区是 4G,垃圾回收器用的是 parNew + CMS。web
首先是查看当前 GC 的状况,主要是使用 jstat
查看 GC 的概况,再查看 gc log,分析单次 gc 的详细情况。面试
使用 jstat -gcutil pid 1000
每隔一秒打印一次 gc 统计信息。spring
能够看到,单次 gc 平均耗时是 60ms 左右,还算能够接受,但 YGC 很是频繁,基本上每秒一次,有的时候还会一秒两次,在一秒两次的时候,服务对业务响应时长的压力就会变得很大。性能优化
接着查看 gc log,打印 gc log 须要在 JVM 启动参数里添加如下参数:并发
-XX:+PrintGCDateStamps
:打印 gc 发生的时间戳。-XX:+PrintTenuringDistribution
:打印 gc 发生时的分代信息。-XX:+PrintGCApplicationStoppedTime
:打印 gc 停顿时长-XX:+PrintGCApplicationConcurrentTime
:打印 gc 间隔的服务运行时长-XX:+PrintGCDetails
:打印 gc 详情,包括 gc 前/内存等。-Xloggc:../gclogs/gc.log.date
:指定 gc log 的路径看到的 gc log 形如:intellij-idea
单次 GC 方面并不能直接看出问题,但能够看到 gc 前有不少次 18ms 左右的停顿。ide
直接查看 gc log 并不直观,咱们能够借用一些可视化工具来帮助咱们分析, [gceasy](https://gceasy.io/)
是个挺不错的网站,咱们把 gc log 上传上去后, gceasy 能够帮助咱们生成各个维度的图表帮助分析。
查看 gceasy 生成的报告,发现咱们服务的 gc 吞吐量是 95%,它指的是 JVM 运行业务代码的时长占 JVM 总运行时长的比例,这个比例确实有些低了,运行 100 分钟就有 5 分钟在执行 gc。幸亏这些 GC 中绝大多数都是 YGC,单次时长可控且分布平均,这使得咱们服务还能平稳运行。
解决这个问题要么是减小对象的建立,要么就增大 young 区。前者不是一时半会儿都解决的,须要查找代码里可能有问题的点,分步优化。
然后者虽然改一下配置就行,但以咱们对 GC 最直观的印象来讲,增大 young 区,YGC 的时长也会迅速增大。
其实这点没必要太过担忧,咱们知道 YGC 的耗时是由 GC 标记 + GC 复制
组成的,相对于 GC 复制,GC 标记是很是快的。而 young 区内大多数对象的生命周期都很是短,若是将 young 区增大一倍,GC 标记的时长会提高一倍,但到 GC 发生时被标记的对象大部分已经死亡, GC 复制的时长确定不会提高一倍,因此咱们能够放心增大 young 区大小。
因为低内存旧机器都被换掉了,我把堆大小调整到了 12G,young 区保留为 8G。
除了 GC 太频繁以外,GC 后各分代的平均大小也须要调整。
咱们知道 GC 的提高机制,每次 GC 后,JVM 存活代数大于 MaxTenuringThreshold
的对象提高到老年代。固然,JVM 还有动态年龄计算的规则:按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,做为新的晋升年龄阈值,但看各代总的内存大小,是达不到 survivor 区的一半的。
因此这十五个分代内的对象会一直在两个 survivor 区之间来回复制,再观察各分代的平均大小,能够看到,四代以上的对象已经有一半都会保留到老年区了,因此能够将这些对象直接提高到老年代,以减小对象在两个 survivor 区之间复制的性能开销。
因此我把 MaxTenuringThreshold 的值调整为 4,将存活超过四代的对象直接提高到老年代。
还有一个问题是 gc log 里有不少 18ms 左右的停顿,有时候连续有十多条,虽然每次停顿时长不长,但连续屡次累积的时间也很是可观。
1.8 以后 JVM 对锁进行了优化,添加了偏向锁的概念,避免了不少没必要要的加锁操做,但偏向锁一旦遇到锁竞争,取消锁须要进入 safe point
,致使 STW。
解决方式很简单,JVM 启动参数里添加 -XX:-UseBiasedLocking
便可。
调整完 JVM 参数后先是对服务进行压测,发现性能确实有提高,也没有发生严重的 GC 问题,以后再把调整好的配置放到线上机器进行灰度,同时收集 gc log,再次进行分析。
因为 young 区大小翻倍了,因此 YGC 的频率减半了,GC 的吞量提高到了 97.75%。平均 GC 时长略有上升,从 60ms 左右提高到了 66ms,仍是挺符合预期的。
因为 CMS 在进行 GC 时也会清理 young 区,CMS 的时长也受到了影响,CMS 的最终标记和并发清理阶段耗时增长了,也比较正常。
另外我还统计了对业务的影响,以前由于 GC 致使超时的请求大大减小了。
总之,这是一次挺成功的 GC 调整,让我对 GC 有了更深的理解,但因为没有深刻到 old 区,以前学习到的 CMS 相关的知识尚未复习到。
不过性能优化并非一朝一夕的事,须要时刻关注问题,及时作出调整。
近期热文推荐:
1.600+ 道 Java面试题及答案整理(2021最新版)
2.终于靠开源项目弄到 IntelliJ IDEA 激活码了,真香!
3.阿里 Mock 工具正式开源,干掉市面上全部 Mock 工具!
4.Spring Cloud 2020.0.0 正式发布,全新颠覆性版本!
以为不错,别忘了随手点赞+转发哦!