JVM性能分析与调优

坚持性能测试——不要用你的眼睛去优化性能 “ —- 周树人

0x00. 前言

JVM和性能调优相关的知识很重要, 前置知识太多, 只能后续再补, 我也是且行且积累, 先直接先写了..

首先工具类产品千变万化, 但是归根到底就那几个派系, 所以我们优先选择掌握原生自带的, 社区活跃的, 功能强大的JVM工具, 以下是我简单分了三类, 都是很棒很好用的, 要会熟练使用, 实际的战斗中, 掌握这三类兵器是解决疑难问题的拿手武器:

  • 可视化监控: 火焰图(必会) 、 JProfiler(推荐)、JConsole + Java VisualVM(原生已带)
  • 原生命令: jstack、jmap、jhat、jstat (必会)
  • 字节码热修改: Arthas (必会)

当然我对JVM和性能调优的理解也很浅薄, 可能还有不少好用/高级的方式没有了解过, 写文章很多时候就是秉持着”我为人人, 人人为我”的分享精神, 希望大家多多指教, ~

0x01. JVM参数

1. 修改JVM参数

先说能动态修改的, 借助jinfo 命令, 可以热修改部分参数, 值得推荐.

1
2
#首先看看哪些参数是可以动态修改的.以及他们目前的状态
java -XX:+PrintFlagsFinal -version | grep manageable

可以看到下面有一些``PrintGC` 相关的参数, 这些对我们调优JVM很有用 ,试试动态打开.

1
2
3
4
5
6
7
8
#注意有些参数是直接设置值的,但是bool的参数是通过参数名前的+/-表示开关
jinfo -flag PrintGCDetails pid #输出当前状况
jinfo -flag PrintGC pid

#修改为开gc日志
jinfo -flag +PrintGC pid #PrintGCDetails同理
#修改为关闭gc日志
jinfo -flag -PrintGC pid

但是这有个很难受的事就是, 你发现有些时候你一通操作, gc日志实际完全没受到影响… 遇到jinfo不生效的话, 还是别用jinfo调了…

然后贴一个我综合了Cassandra官方+ElasticSearch官方jvm8.optionG1GC配置 , 主要适用于存储/计算为主的底层组件[比如图] (机器内存 > 64G) ,解释参考

1
2
3
4
5
6
7
# heap memory should < 32G because jvm's obj-pointer may be out of control (referrence ElasticSearch↑)
MAX_MEM="31"
MIN_MEM="31"
# use G1 gc and print GClog clearly
JAVA_OPTIONS="-Xms${MIN_MEM}g -Xmx${XMX}g -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:InitiatingHeapOccupancyPercent=70 \
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:G1RSetUpdatingPauseTimePercent=5 \
-XX:+UseGCLogFileRotation -XX:GCLogFileSize=5M -XX:NumberOfGCLogFiles=5 -Xloggc:./logs/gc.log -XX:+PrintHeapAtGC"

待补充…

0x02. jstack

jstack主要涉及java线程相关的知识, 这个之后会单独补充一下, 先给个最常用的jstack使用方法 :

1
2
#打印100次jstack, 然后每隔10条看一次. 如果时间间隔长,可以加比如 && sleep 10s
for i in {0..100};do jstack pid > s$i;done

然后打出了一堆jstack信息, 就是学会如何去看了.

待补充…

0x03.火焰图

*” 童年时代,每逢冬天和家人围坐在火堆旁边烤火,我就不由想起了壁炉里熊熊燃烧的火焰的形态。火焰图也就是这样来的(:”* —- 鲁迅

火焰首先从燃起, 然后以火苗的形式往上面窜升. 把从靠近根到顶部的每个火苗,想象成一个调用栈, 由于火苗有许多根,这也就和进程执行逻辑相似了.

firePic000

那进入正题, 火焰图需要重点掌握, 因为它是跨平台, 跨语言的性能分析利器. 适用于JS, C/C++, Python, Java, PHP, GO 等等语言, 并一般都有专用的版本, 而且在Unix 上可通用的监控某个进程PID来快速找到这个进程某段时间在做什么, 并生成一张可视化的SVG图, 从而帮助我们快速找到性能瓶颈. 关于火焰图的基本使用和普及参考:

然后原生的火焰图工具使用比较繁琐, 这里我们用的是在原生基础上做了封装和优化的Java专用版-async-profiler1.5 ,就很易用了.

Linux包下载到你需要监控的机器上, 不同于jstack 的100次踩点打印, 火焰图的主要是帮你找到程序运行(比如60s)期间, 整体上哪个方法耗时最长, 然后形成一个直观的图来展示(IDEAMac版上已经内置支持了), 细节分析则依靠”JProfile + Arthas”

最简单的使用就一条命令, 指定pid和监控的时间, 然后生成火焰图的路径, 并且如果程序提前终止了 ,这边也会自动停止, 需要注意的是, 不同实现的增强版火焰图里, 可能与原生的设计是稍有一些变化的, 例如火焰颜色可能就是有区别, 有意义的 (不能听风就是雨), 比如下图中, 我观察推测(官方没找到具体说明)如下:

  • 绿色: 代表java执行的代码 (主要)
  • 黄色: 代表C/C++一类的Native代码, 包括编译器/GC执行 (次要)
  • 红色: 代表系统调用层, 或是未知的执行代码段.. (连接?)
  • 其他: 未知..

以上如果有官方的说法, 或者偏差的地方请及时告知, 多多指教~

SwingSetFire

1
2
3
4
5
#把60s内的进程请求转化为火焰图.
./profiler.sh pid -d 60 -f ~/watch/test.svg #需要注意的是这个输出路径不能使用相对路径, 原因参考issue#190

#发送到PC机查看,当然也可用scp
sz test.svg

然后实际的使用就需要结合具体的案例来说了. 比如程序更新了几个新功能后你发现GC次数/时间锐增, 或是内存占用明显变大, 又或是运行速度有明显下滑, 这些场景都非常适合同一环境下, 打两个火焰图对比, 一般就能很快找到思路. 而不是对着修改的几千行代码乱看, 没有头绪的去找到底哪里写的有问题..

更新: 典型实例和综合运用参考下一篇一次图Hugegraph-Loader性能问题的分析

0x04. jmap

一般来说, 出现内存泄露相关的问题的时候, 就离不开使用jmap这个命令查看/导出当前的堆的具体使用情况, 然后再把堆文件丢给其他分析工具进行分析, 所以是必须得会用的.

1
2
3
4
5
6
7
8
#最常见的三板斧:
#1.查看当前pid的内存状态(live会触发一次fullGC,可去)
jmap -histo:live pid | less

#2.如果没有发现答案,那么导出当前堆,注意选择一个空间足够的地方
jmap -dump:format=b,file=/path/to/pnameDump.hprof pid

#3.本地使用Jprofile 或 MAT加载这个堆文件.(MAT只在eclipse上有)

补充: 实际用法可参考一次内存泄露问题的解决过程.

0x05. JProfiler

JProfiler可以看做是”原生性能监控/分析 + MAT” 的综合加强版, 功能强大, 界面美观, 类似IDEA, 它本身是一个商业软件, 所以需要自己动手丰衣足食安装并与IDEA集成…. 后面说的内容都是基于19年最新的Jprofile 11版本. (JDK 1.8), 这是软件的界面全览. 左侧前几个是核心模块:

jprofile01

从上图中, 我们就能发现JProfiler到底关注哪几个方面 :

  1. 方法调用分析 : 实时分析包括时延, CPU/内存使用, GC细节等
  2. 内存分析: 离线/在线的分析堆内存中的 “对象 , 引用链 ,GC” , 帮助找到内存溢出/泄露的元凶, 在线分析可以优化内存使用.
  3. 线程分析: 对多线程和各种锁已经线程状态直观展示.
  4. 其他系统集成: 这也就是上图中后三个的体现, 比如Socket组件里连接最容易中断的点, Web服务里启动耗时最长的地方, DB里我想找出执行速度最慢的SQL语句等. (不过我在想这个直接在db里复现不是更快么?)

了解了基本功能后, 再来看看整体架构:

jprofile00

初看可能除了UI 模块, 其他都是一脸懵逼, 所以强烈建议看图前, 先阅读一下这篇深入浅出JProfiler - 使用原理, 这里再简单说一下核心的几个模块:

1.Agent

关键点就是这个JProfiler Agent (现在这种方式也很流行), 不管是在本地还是远端, 它可以通过JVM启动的时候添加-agentpath:/path/to/libxx,xxx 来附着在JVM上, 成功后它会动态注入一些性能监控操作的字节码, 用以各种JVM参数监控

2.UI

比较好理解, 它负责把agent采集到的信息可视化展示. 要注意的是, 不管是本地还是远端, UI都是通过socket 与agent通信, 所以没有本质区别.

3.Command

除了可视化展示, JProfiler自身依靠一系列命令实现功能, 主要是控制agent进行各种操作(比如图上的jcontroller/jpdump等). 需要可参考官方文档.这不多介绍

这样整个JProfiler 我想从整体功能+结构上, 就有了一个比较直观的认识, 再来使用它就不会觉得很陌生. 不知如何下手了~ (也可以从类似的事中总结一些经验.)

然后我们定位线上问题, 多是使用JProfile远程连接, 同时需要下载Linux端的agent版, 用它”嵌入”到需要观察的JVM中, 然后通过本地的JProfile去获取agent的信息可视化展示. (顺带一提的是: Linux版解压后/doc/JProfiler.pdf 是很详细的官方文档, 推荐参考, 比网上搜索靠谱的多, 这是在线版)

配置好后, 就开始进入正题了, 首先agent有两种监控方式:

  1. (默认)Sampling : 在初用的时候你都可以用这个方式, 对性能影响小, 能收集大部分数据 (但不一定很精准, 也不能统计到很细的信息统计: 比如方法级别的)
  2. Instrumentation: 其实就是完整模式, 缺点是默认会显示所有类, 你需要自己配置过滤掉许多, 熟悉了之后应该都可以用这种模式.

6月13更新: 发现有个系列写的还不错, 虽然大部分是翻译的官方文档, 但是也很有参考价值, 想了解的同学可以先看这个, 后续我会结合具体的例子来讲, 因为原理分析耗时比较多, 可能暂时填坑比较慢.

未完待续…..

0x06. 其他工具

除了JVM相关和Linux相关的工具外, IDEA本身也有一些很好用的工具, 帮助我们提高开发效率, 比如著名的Cloud Toolkit :

它是做什么的呢? 看个官方对比图就很清楚了:

  • 传统部署流程 (繁琐. 或需重度依赖Drone/Jenkins 类CI/CD工具)

    cloudToolkit00

  • Cloud Toolkit流程 (适合个人开发 & 无CI/CD环境)

    cloudToolkit01

    然后根据不同的服务器环境, 它提供了上图右侧对应的多种一键部署配置方案, 常见有三种:

    1. 公有云
    2. 内网服务器 (常用)
    3. Docker + K8s

    第一次配置好后, 后面都是一键操作了, 而且就算你本地无法编译, 它也可以上传文件, 然后你在服务端自动编译打包分发

    而且更为方便的是, 它内置了SSH-bash, 并自带了Arthas 这个强大的性能定位工具, 可以直接一键启动, 普通问题甚至不用单独启动Xshell . 就能在IDEA里完成快速的问题定位 + 测试, 如官方下图所示:

    cloudToolkit02

注意事项 : 一键部署执行脚本/命令的时候, 有一些不能使用, CMD编写参考

7.25更新: 基于Linux的通用性能分析和监控拆分于此.


参考资料:
  1. gc日志学习笔记
  2. jstack使用经验总结
  3. IBM-应用性能调优实践
  4. JProfile介绍与实践
  5. Cloud Toolkit介绍和使用
  6. 火焰图相关