性能调优
应用程序在运行过程中经常会出现性能问题,比较常见的性能问题现象是:
- 通过top命令查看CPU占用率高,接近100甚至多核CPU下超过100都是有可能的。
- 请求单个服务处理时间特别长,多服务使用skywalking等监控系统来判断是哪一个环节性能低下。
- 程序启动之后运行正常,但是在运行一段时间之后无法处理任何的请求(内存和GC正常)。
线程转储
线程转储(Thread Dump)提供了对所有运行中的线程当前状态的快照。线程转储可以通过jstack、visualvm等工具获取。其中包含了线程名、优先级、线程ID、线程状态、线程栈信息等等内容,可以用来解决CPU占用率高、死锁等问题
线程转储(Thread Dump)中的几个核心内容:
- 名称:线程名称,通过给线程设置合适的名称更容易“见名知意”
- 优先级(prio):线程的优先级
- Java ID(tid):JVM中线程的唯一ID
- 本地 ID (nid):操作系统分配给线程的唯一ID
- 状态:线程的状态,分为: NEW – 新创建的线程,尚未开始执行 RUNNABLE –正在运行或准备执行 BLOCKED – 等待获取监视器锁以进入或重新进入同步块/方法 WAITING – 等待其他线程执行特定操作,没有时间限制 TIMED_WAITING – 等待其他线程在指定时间内执行特定操作 TERMINATED – 已完成执行
- 栈追踪: 显示整个方法的栈帧信息
线程转储的可视化在线分析平台:https://fastthread.io/ (需要科学上网)
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)找到栈信息对应的源代码,并分析问题产生原因。
接口响应时间长的问题
已经确定是某个接口性能出现了问题,但是由于方法嵌套比较深,需要借助于arthas定位到具体的方法。
使用arthas的trace命令,可以展示出整个方法的调用路径以及每一个方法的执行耗时。
命令:trace 类名 方法名
- 添加 --skipJDKMethod false 参数可以输出JDK核心包中的方法及耗时。
- 添加 ‘#cost > 毫秒值’ 参数,只会显示耗时超过该毫秒值的调用。
- 添加 –n 数值 参数,最多显示该数值条数的数据。
- 所有监控都结束之后,输入stop结束监控,重置arthas增强的对象。
在使用trace定位到性能较低的方法之后,使用watch命令监控该方法,可以获得更为详细的方法信息。
命令: watch 类名 方法名 ‘{params, returnObj}’ ‘#cost>毫秒值' -x 2
- ‘{params, returnObj}‘ 代表打印参数和返回值。
- -x 代表打印的结果中如果有嵌套(比如对象里有属性),最多只展开2层。允许设置的最大值为4。
最后记得使用stop命令将所有增强的对象恢复。
JMH
OpenJDK中提供了一款叫JMH(Java Microbenchmark Harness)的工具,可以准确地对Java代码进行基准测试, 量化方法的执行性能。
JMH会首先执行预热过程,确保JIT对代码进行优化之后再进行真正的迭代测试,最后输出测试的结果。
1)引入依赖
<dependencies><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>1.33</version> <!-- 使用最新的版本 --></dependency><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>1.33</version> <!-- 使用最新的版本 --></dependency>
</dependencies>
2)编写测试代码
java">package cn.ehang;import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;import java.util.concurrent.TimeUnit;@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
public class Main {String string = "";StringBuilder stringBuilder = new StringBuilder();@Benchmarkpublic String stringAdd() {for (int i = 0; i < 1000; i++) {string = string + i;}return string;}@Benchmarkpublic String stringBuilderAppend() {for (int i = 0; i < 1000; i++) {stringBuilder.append(i);}return stringBuilder.toString();}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(Main.class.getSimpleName()).build();new Runner(opt).run();}
}
3)测试结果
# JMH version: 1.33
# VM version: JDK 17.0.11, Java HotSpot(TM) 64-Bit Server VM, 17.0.11+7-LTS-207
# VM invoker: C:\Program Files\Java\jdk-17\bin\java.exe
# VM options: -Dvisualvm.id=11302190666600 -javaagent:D:\software\IntelliJ IDEA 2024.1.4\lib\idea_rt.jar=50868:D:\software\IntelliJ IDEA 2024.1.4\bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint (default, use -Djmh.blackhole.autoDetect=true to auto-detect)
# Warmup: 3 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: cn.ehang.Main.stringAdd# Run progress: 0.00% complete, ETA 00:02:40
# Fork: 1 of 1
# Warmup Iteration 1: 26.184 ms/op
# Warmup Iteration 2: 60.169 ms/op
# Warmup Iteration 3: 80.841 ms/op
Iteration 1: 103.935 ms/op
Iteration 2: 122.784 ms/op
Iteration 3: 135.320 ms/op
Iteration 4: 147.907 ms/op
Iteration 5: 160.549 ms/opResult "cn.ehang.Main.stringAdd":134.099 ±(99.9%) 84.575 ms/op [Average](min, avg, max) = (103.935, 134.099, 160.549), stdev = 21.964CI (99.9%): [49.524, 218.674] (assumes normal distribution)