09性能调优
性能优化的步骤总共分为四个步骤,其中修复部分要具体问题具体分析且处理方式各不相同。本章中着重学习发现问题和诊断问题的方法,目标是准确定位到性能问题的根源。
- 发现:通过监控、测试工具发现性能问题。
- 诊断:通过分析工具,定位到某一部分代码存在性能问题。
- 修复:修复问题。
- 验证:测试验证。

1.性能调优解决的问题
应用程序在运行过程中经常会出现性能问题,比较常见的性能问题现象是:
1、通过 top 命令查看 CPU 占用率高,接近 100 甚至多核 CPU 下超过 100 都是有可能的。CPU 使用率达到接近 100%。
2、请求单个服务处理时间特别长,多服务使用 skywalking 等监控系统来判断是哪一个环节性能低下。Jmeter 单个请求耗时超过 4 秒。
3、程序启动之后运行正常,但是在运行一段时间之后无法处理任何的请求(内存和 GC 正常)。
2.性能调优的方法
2.1 线程转储
线程转储(Thread Dump)提供了对所有运行中的线程当前状态的快照。线程转储可以通过 jstack、visualvm 等工具获取。其中包含了线程名、优先级、线程 ID、线程状态、线程栈信息等等内容,可以用来解决 CPU 占用率高、死锁等问题。

线程转储(Thread Dump)中的几个核心内容:
- 名称:线程名称,通过给线程设置合适的名称更容易"见名知意"。
- 优先级(prio):线程的优先级。
- Java ID(tid):JVM 中线程的唯一 ID。
- 本地 ID(nid):操作系统分配给线程的唯一 ID。
- 状态:线程的状态,分为:
- NEW – 新创建的线程,尚未开始执行
- RUNNABLE – 正在运行或准备执行
- BLOCKED – 等待获取监视器锁以进入或重新进入同步块/方法
- WAITING – 等待其他线程执行特定操作,没有时间限制
- TIMED_WAITING – 等待其他线程在指定时间内执行特定操作
- TERMINATED – 已完成执行
- 栈追踪:显示整个方法的栈帧信息。
线程转储的可视化在线分析平台:
1、jstack.review - Java Thread Dump Analyzer
2、Smart Java thread dump analyzer - thread dump analysis in seconds
2.2 解决 CPU 占用率高的问题思路
1、通过 top –c 命令找到 CPU 占用率高的进程,获取它的进程 ID。
2、使用 top -p 进程ID 单独监控某个进程,按 H 可以查看到所有的线程以及线程对应的 CPU 使用率,找到 CPU 使用率特别高的线程。
3、使用 jstack 进程ID 命令可以查看到所有线程正在执行的栈信息。使用 jstack 进程ID > 文件名 保存到文件中方便查看。
4、找到 nid 线程 ID 相同的栈信息,需要将之前记录下的十进制线程号转换成 16 进制。通过 printf '%x\n' 线程ID 命令直接获得 16 进制下的线程 ID。
5、找到栈信息对应的源代码,并分析问题产生原因。
在定位 CPU 占用率高的问题时,比较需要关注的是状态为 RUNNABLE 的线程。但实际上,有一些线程执行本地方法时并不会消耗 CPU,而只是在等待(比如等待 socket 读取数据,IO 等待不消耗 CPU)。但 JVM 仍然会将它们标识成"RUNNABLE"状态。
2.3 方法嵌套比较深的情况下,找到具体哪个方法 CPU 占用率高
上面已经确定是某个接口性能出现了问题,但是由于方法嵌套比较深(A 方法 -> B 方法 -> C 方法 -> D 方法),需要借助于 arthas 定位到具体的方法。整体耗时较长,我们需要定位出来是 C 方法慢导致的问题。
trace 命令监控
使用 arthas 的 trace 命令,可以展示出整个方法的调用路径以及每一个方法的执行耗时。
命令:trace 类名 方法名
- 添加
--skipJDKMethod false参数可以输出 JDK 核心包中的方法及耗时。 - 添加
'#cost > 毫秒值'参数,只会显示耗时超过该毫秒值的调用。 - 添加
-n 数值参数,最多显示该数值条数的数据。 - 所有监控都结束之后,输入
stop结束监控,重置 arthas 增强的对象。
操作步骤:
1、使用 trace 命令监控方法的执行。
2、发起一次请求调用。
3、显示出了方法调用的耗时占比。
4、添加 --skipJDKMethod false 参数可以输出 JDK 核心包中的方法及耗时。
5、添加 '#cost > 1000' 参数,只显示耗时超过 1 秒的调用。
6、添加 -n 1 参数,最多显示 1 条数据,避免数据太多看起来不清晰。
7、所有监控都结束之后,输入 stop 结束监控,重置 arthas 增强的对象,避免对性能产生影响。
watch 命令监控
在使用 trace 定位到性能较低的方法之后,使用 watch 命令 监控该方法,可以获得更为详细的方法信息。
命令:
watch 类名 方法名 '{params, returnObj}' '#cost>毫秒值' -x 2'{params, returnObj}'代表打印参数和返回值。-x代表打印的结果中如果有嵌套(比如对象里有属性),最多只展开 2 层。允许设置的最大值为 4。
执行命令后发起一笔接口调用,cost = 1565ms 代表方法执行时间是 1.56 秒,result = 后边是参数的内容,首先是一个集合(既可以获取返回值,也可以获取参数),第一个数组就是参数,里边只有一个元素是一个整数值为 1。
总结:
1、通过 arthas 的 trace 命令,首先找到性能较差的具体方法,如果访问量比较大,建议设置最小的耗时,精确的找到耗时比较高的调用。
2、通过 watch 命令,查看此调用的参数和返回值,重点是参数,这样就可以在开发环境或者测试环境模拟类似的现象,通过 debug 找到具体的问题根源。
3、使用 stop 命令将所有增强的对象恢复。
3.定位偏底层的性能问题
有一个接口中使用了 for 循环向 ArrayList 中添加数据,但是最终发现执行时间比较长,需要定位是由于什么原因导致的性能低下。
解决思路:Arthas 提供了性能火焰图的功能,可以非常直观地显示所有方法中哪些方法执行时间比较长。

使用 arthas 的 profiler 命令,生成性能监控的火焰图。
- 命令 1:
profiler start开始监控方法执行性能。 - 命令 2:
profiler stop --format html以 HTML 的方式生成火焰图。
火焰图中一般找绿色部分 Java 中栈顶上比较平的部分,很可能就是性能的瓶颈。
操作步骤:
1、使用命令开始监控。
2、发送请求测试。
3、执行命令结束,并生成火焰图的 HTML。
4、观察火焰图的结果。
火焰图中重点关注左边部分,是我们自己编写的代码的执行性能,右边是 Java 虚拟机底层方法的性能。火焰图中会展示出 Java 虚拟机自身方法执行的时间。火焰图中越宽的部分代表执行时间越长,很明显 ArrayList 类中的 add 方法调用花费了大量的时间,这其中可以发现一个 copyOf 方法,数组的拷贝占用时间较多。
观察源码可以知道,频繁的扩容需要多次将老数组中的元素复制到新数组,浪费了大量的时间。在 ArrayList 的构造方法中,设置一下最大容量,一开始就让它具备这样的大小,避免频繁扩容带来的影响。最终这部分开销就没有了。
总结: 偏底层的性能问题,特别是由于 JDK 中某些方法被大量调用导致的性能低下,可以使用火焰图非常直观地找到原因。
这个案例中是由于创建 ArrayList 时没有手动指定容量,导致使用默认的容量而在添加对象过程中发生了多次的扩容,扩容需要将原来数组中的元素复制到新的数组中,消耗了大量的时间。通过火焰图可以看到大量的调用,修复完之后节省了 20% ~ 50% 的时间。
4.线程耗尽问题/死锁问题
问题:程序在启动运行一段时间之后,就无法接受任何请求了。将程序重启之后继续运行,依然会出现相同的情况。
解决思路: 线程耗尽问题一般是由于执行时间过长,分析方法分成两步:
1、检测是否有死锁产生,无法自动解除的死锁会将线程永远阻塞。
2、如果没有死锁,再使用上面的打印线程栈的方法检测线程正在执行哪个方法,一般这些大量出现的方法就是慢方法。
线程死锁可以通过三种方法定位问题:
1、jstack -l 进程ID > 文件名 将线程栈保存到本地,在文件中搜索 deadlock 即可找到死锁位置。
2、开发环境中使用 visual vm 或者 Jconsole 工具,都可以检测出死锁。使用线程快照生成工具就可以看到死锁的根源。生产环境的服务一般不会允许使用这两种工具连接。
3、使用 fastthread 自动检测线程问题。https://fastthread.io/。Fastthread 和 Gceasy 类似,是一款在线的 AI 自动线程问题检测工具,可以提供线程分析报告。通过报告查看是否存在死锁问题。在 visualvm 中保存线程栈,选择文件并点击分析,即可得到死锁分析报告。