一次简单的 JVM 调优,性能提升了15%

简介: 最近对负责的项目进行了一次性能优化,其中包括对 JVM 参数的调整,算是进行了一次简单的 JVM 调优,JVM 参数调整之后,服务的整体性能有 15% 左右的提升,还算不错。先介绍一下项目的基本情

背景

最近对负责的项目进行了一次性能优化,其中包括对 JVM 参数的调整,算是进行了一次简单的 JVM 调优,JVM 参数调整之后,服务的整体性能有 15% 左右的提升,还算不错。

先介绍一下项目的基本情况:

项目是一个高 QPS 压力的 web 服务,单机 QPS 一直维持在 1.5K 以上,由于旧机器的”拖累”,配置的堆大小是 8G,其中 young 区是 4G,垃圾回收器用的是 parNew + CMS。

旧状

首先是查看当前 GC 的情况,主要是使用 jstat 查看 GC 的概况,再查看 gc log,分析单次 gc 的详细状况。

使用 jstat -gcutil pid 1000 每隔一秒打印一次 gc 统计信息。

image.png

可以看到,单次 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 形如:

image.png

单次 GC 方面并不能直接看出问题,但可以看到 gc 前有很多次 18ms 左右的停顿。

分析和调整

YGC 频繁

直接查看 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 后各分代的平均大小也需要调整。

image.png

我们知道 GC 的提升机制,每次 GC 后,JVM 存活代数大于 MaxTenuringThreshold 的对象提升到老年代。当然,JVM 还有动态年龄计算的规则:按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值,但看各代总的内存大小,是达不到 survivor 区的一半的。

image.png

所以这十五个分代内的对象会一直在两个 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 相关的知识还没有复习到。不过性能优化并不是一朝一夕的事,需要时刻关注问题,及时做出调整。

相关文章
|
2月前
|
Arthas 监控 Java
(十一)JVM成神路之性能调优篇:GC调优、Arthas工具详解及各场景下线上最佳配置推荐
“在当前的互联网开发模式下,系统访问量日涨、并发暴增、线上瓶颈等各种性能问题纷涌而至,性能优化成为了现时代开发过程中炙手可热的名词,无论是在开发、面试过程中,性能优化都是一个常谈常新的话题”。
160 3
|
2月前
|
监控 Java 测试技术
JVM 性能调优 及 为什么要减少 Full GC
JVM 性能调优 及 为什么要减少 Full GC
84 4
|
2月前
|
运维 Java Linux
(九)JVM成神路之性能调优、GC调试、各内存区、Linux参数大全及实用小技巧
本章节主要用于补齐之前GC篇章以及JVM运行时数据区的一些JVM参数,更多的作用也可以看作是JVM的参数列表大全。对于开发者而言,能够控制JVM的部分也就只有启动参数了,同时,对于JVM的性能调优而言,JVM的参数也是基础。
|
2月前
|
缓存 监控 Java
Java虚拟机(JVM)性能调优实战指南
在追求软件开发卓越的征途中,Java虚拟机(JVM)性能调优是一个不可或缺的环节。本文将通过具体的数据和案例,深入探讨JVM性能调优的理论基础与实践技巧,旨在为广大Java开发者提供一套系统化的性能优化方案。文章首先剖析了JVM内存管理机制的工作原理,然后通过对比分析不同垃圾收集器的适用场景及性能表现,为读者揭示了选择合适垃圾回收策略的数据支持。接下来,结合线程管理和JIT编译优化等高级话题,文章详细阐述了如何利用现代JVM提供的丰富工具进行问题诊断和性能监控。最后,通过实际案例分析,展示了性能调优过程中可能遇到的挑战及应对策略,确保读者能够将理论运用于实践,有效提升Java应用的性能。 【
155 10
|
2月前
|
监控 算法 Java
深入理解Java虚拟机:JVM调优的实用策略
在Java应用开发中,性能优化常常成为提升系统响应速度和处理能力的关键。本文将探讨Java虚拟机(JVM)调优的核心概念,包括垃圾回收、内存管理和编译器优化等方面,并提供一系列经过验证的调优技巧。通过这些实践指导,开发人员可以有效减少延迟,提高吞吐量,确保应用稳定运行。 【7月更文挑战第16天】
|
2月前
|
JSON Java BI
一次Java性能调优实践【代码+JVM 性能提升70%】
这是我第一次对系统进行调优,涉及代码和JVM层面的调优。如果你能看到最后的话,或许会对你日常的开发有帮助,可以避免像我一样,犯一些低级别的错误。本次调优的代码是埋点系统中的报表分析功能,小公司,开发结束后,没有Code Review环节,所以下面某些问题,也许在Code Review环节就可以避免。
137 0
一次Java性能调优实践【代码+JVM 性能提升70%】
|
1月前
|
监控 算法 Java
深入理解Java虚拟机:JVM调优与性能提升
本文旨在为Java开发者提供一条清晰的路径,以深入掌握Java虚拟机(JVM)的内部机制和性能调优技巧。通过具体案例分析,我们将探讨如何识别性能瓶颈、选择合适的工具进行监控与调试,以及实施有效的优化策略,最终达到提高应用程序性能的目的。文章不仅关注理论,更注重实践应用,帮助读者在面对复杂的Java应用时能够游刃有余。
49 0
|
1月前
|
Arthas Prometheus 监控
使用JDK自带工具调优JVM的常用命令
使用JDK自带工具调优JVM的常用命令
|
1月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
30天前
|
存储 算法 Oracle
不好意思!耽误你的十分钟,JVM内存布局还给你
先赞后看,南哥助你Java进阶一大半在2006年加州旧金山的JavaOne大会上,一个由顶级Java开发者组成的周年性研讨会,公司突然宣布将开放Java的源代码。于是,下一年顶级项目OpenJDK诞生。Java生态发展被打开了新的大门,Java 7的G1垃圾回收器、Java 8的Lambda表达式和流API…大家好,我是南哥。一个Java学习与进阶的领路人,相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。
不好意思!耽误你的十分钟,JVM内存布局还给你