Skip to content

第69课:性能分析工具

🎯 学习目标

  • 理解性能优化必须先测量再优化,不能凭感觉改代码。
  • 掌握吞吐量、延迟、错误率、资源利用率等核心指标。
  • 能使用 JDK 自带工具定位线程、内存、GC、类加载和 JVM 参数问题。
  • 了解 JFR/JMC、Arthas、async-profiler、Micrometer 等常用工具的适用场景。
  • 能建立一套从现象到根因的性能排查流程。

📖 一、性能分析的基本原则

性能优化最常见的错误是“看到一段代码觉得慢,然后开始改”。正确流程应该是:

text
定义目标 -> 收集指标 -> 复现问题 -> 定位瓶颈 -> 小步优化 -> 对比验证 -> 持续监控

性能问题通常不是单点问题,而是多个因素叠加:

text
接口慢:可能是 SQL 慢、锁竞争、线程池满、GC 停顿、远程调用慢、连接池耗尽。
CPU 高:可能是死循环、序列化过重、正则回溯、热点方法、频繁 GC。
内存高:可能是缓存无界、集合未清理、线程本地变量泄漏、对象创建过多。

没有指标,优化就是猜测。


📖 二、核心性能指标

指标含义关注点
QPS/TPS每秒处理请求或事务数系统吞吐能力
平均响应时间请求平均耗时容易被极端值掩盖
P95/P9995%/99% 请求的响应时间更能反映用户体验
错误率请求失败比例性能压测中必须同时观察
CPU 使用率计算资源占用高 CPU 不一定是坏事,长期 100% 才危险
内存使用堆、非堆、直接内存看趋势和 GC 后是否回落
GC 暂停垃圾回收停顿时间影响尾延迟
线程数活跃线程、阻塞线程判断线程池和锁竞争
连接池数据库/Redis/HTTP 连接占用连接耗尽会拖慢全局

尤其要关注 P95/P99。平均耗时 50ms 并不代表系统健康,因为可能 99% 的用户都在等 2 秒。


📖 三、JDK 自带工具

1. jps:查看 Java 进程

bash
jps -lv

输出示例:

text
12345 com.example.Application -Xms512m -Xmx512m

2. jcmd:综合诊断入口

bash
jcmd 12345 VM.flags
jcmd 12345 VM.system_properties
jcmd 12345 Thread.print
jcmd 12345 GC.heap_info

jcmd 是现代 JDK 中非常推荐的入口工具,很多老工具能力都可以通过它完成。

3. jstack:线程快照

bash
jstack 12345 > thread.txt

重点查看:

text
BLOCKED:锁竞争
WAITING/TIMED_WAITING:等待条件、队列、IO
RUNNABLE:正在运行,可能消耗 CPU
deadlock:死锁信息

如果 CPU 很高,可以结合系统线程 ID 定位热点线程:

bash
top -H -p 12345
printf "%x\n" 线程ID
jstack 12345 | grep -A 30 nid=0x十六进制ID

4. jmap:堆信息和堆转储

bash
jmap -histo:live 12345 | head
jmap -dump:live,format=b,file=heap.hprof 12345

堆转储文件可以用 Eclipse MAT、JProfiler 或 VisualVM 分析。

注意:dump 堆可能暂停应用,并产生很大文件,生产环境要谨慎。

5. jstat:GC 观察

bash
jstat -gcutil 12345 1000 10

表示每 1 秒输出一次,共 10 次。重点看:

text
YGC/YGCT:年轻代 GC 次数和耗时
FGC/FGCT:Full GC 次数和耗时
GCT:总 GC 时间

Full GC 频繁通常是严重信号。


📖 四、JFR 与 JMC

JFR 是 Java Flight Recorder,JMC 是 Java Mission Control。它们适合低开销采集线上 JVM 运行数据。

启动采集:

bash
jcmd 12345 JFR.start name=profile duration=120s filename=profile.jfr settings=profile

停止采集:

bash
jcmd 12345 JFR.stop name=profile

JFR 可以看到:

text
CPU 热点方法
对象分配热点
GC 暂停
锁竞争
线程状态
文件和 Socket IO
异常抛出频率

JFR 的价值在于把 JVM 内部事件放到同一条时间线上,适合排查“某段时间突然变慢”的问题。


📖 五、Arthas

Arthas 是阿里开源的 Java 诊断工具,适合在线排查。

常用命令:

bash
dashboard        # 查看整体状态
thread           # 查看线程
jad 类名          # 反编译类
watch 方法 '{params, returnObj, throwExp}' -x 3
trace 方法     # 查看方法内部调用耗时
monitor 方法   # 统计方法调用次数和耗时

示例:定位某个 Service 方法为什么慢:

bash
trace com.example.OrderService createOrder

它会显示方法内部每个调用节点的耗时,适合快速判断慢在 SQL、远程调用还是本地计算。

注意:watchtrace 对高频方法有性能影响,生产环境要限制次数和条件。


📖 六、async-profiler

async-profiler 常用于 CPU 火焰图和分配火焰图。

CPU 采样:

bash
./profiler.sh -d 60 -e cpu -f cpu.html 12345

对象分配采样:

bash
./profiler.sh -d 60 -e alloc -f alloc.html 12345

火焰图阅读方法:

text
横向宽度:采样占比,越宽越热点
纵向层级:调用栈深度
顶部宽块:最终消耗 CPU 的方法

不要只看最上层业务方法,要顺着调用栈看真正耗时的是序列化、正则、集合操作、锁,还是第三方库。


📖 七、应用层监控

Spring Boot 常用 Micrometer + Actuator 暴露指标:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yaml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus

常见指标:

text
http.server.requests
jvm.memory.used
jvm.gc.pause
jvm.threads.live
hikaricp.connections.active
system.cpu.usage

这些指标可以接入 Prometheus + Grafana,形成趋势图和告警。


📖 八、压测工具

常见压测工具:

工具场景
JMeterGUI 友好,适合复杂压测计划
k6脚本化,适合 CI/CD 和云原生
wrk轻量 HTTP 压测
ab简单快速,但能力有限

压测时必须记录:

text
压测机器配置
目标机器配置
并发数
持续时间
请求数据规模
预热时间
QPS
P95/P99
错误率
CPU/内存/GC/连接池指标

只给一个“QPS 多少”没有意义。


⚠️ 九、常见陷阱

1. 本地结果代表线上

本地机器、数据量、网络、JVM 参数都不同,性能结论不能直接外推到生产。

2. 只看平均值

平均响应时间会掩盖尾延迟。企业系统更关注 P95/P99。

3. 没有基线

优化前没有记录指标,优化后就无法证明有效。

4. 采样工具影响系统

诊断工具也有开销。生产环境使用 trace、堆 dump、频繁日志时要谨慎。


🆚 十、Java vs C 对比

维度C/C++Java
CPU 分析perf、gprof、flamegraphJFR、async-profiler、Arthas
内存问题malloc/free、ASan、Valgrindheap dump、GC 日志、MAT
运行时更接近系统层JVM 运行时提供大量事件
停顿问题通常来自锁/IO还要关注 GC 和 JIT

Java 性能分析不能只看操作系统指标,必须结合 JVM 内部指标。


💡 十一、最佳实践

  • 优化前先定义目标,例如 P99 从 800ms 降到 300ms。
  • 每次只改一个变量,否则无法判断效果来源。
  • 先看大头:SQL、远程调用、锁、GC、连接池,别先抠微优化。
  • 压测必须预热,避免 JIT 和缓存冷启动影响结论。
  • 生产环境长期保留基础指标,问题发生后才有历史对比。
  • 堆 dump、线程 dump、JFR 文件要按时间和版本归档,便于复盘。
  • 性能优化结论必须用数据证明,不用“应该会快”这种表达。

🎓 小结

性能分析工具的价值是帮你把“系统很慢”拆成可验证的问题:慢在哪个接口、哪个阶段、哪个资源、哪个方法。掌握工具不是目的,建立“指标驱动、证据驱动”的排查流程才是核心。


🧭 十二、性能排查剧本

线上接口突然变慢时,可以按这个顺序处理:

text
1. 先确认影响范围:单接口、单实例、单用户、全部流量?
2. 看入口指标:QPS、P95、P99、错误率是否同时变化?
3. 看资源指标:CPU、内存、GC、线程、连接池。
4. 看依赖指标:数据库、Redis、消息队列、HTTP 下游。
5. 抓线程快照:是否大量 BLOCKED 或连接池等待?
6. 抓 JFR 或 profiler:CPU 热点和对象分配热点在哪里?
7. 对比变更:最近是否发布、改配置、改 SQL、扩容或缩容?
8. 小范围验证:灰度回滚或单点修改确认根因。
9. 形成复盘:记录症状、指标、根因、修复和预防措施。

示例判断:

text
CPU 高 + RUNNABLE 线程多:优先看 CPU profiler。
CPU 低 + 线程大量 WAITING:优先看下游等待和连接池。
GC 时间升高 + 内存 GC 后不回落:优先看 heap dump。
P99 升高但平均值稳定:优先看锁竞争、慢 SQL、少量慢依赖。
错误率升高 + 队列堆积:优先看背压和拒绝策略。

📌 十三、工具选择速查

问题首选工具
不知道 Java 进程号jps -lv
看 JVM 参数jcmd &lt;pid&gt; VM.flags
看线程卡在哪里jstack、Arthas thread
看方法调用耗时Arthas trace
看 CPU 热点JFR、async-profiler
看对象分配热点JFR、async-profiler alloc
看堆里谁占内存jmap + MAT
看 GC 频率和暂停GC 日志、jstat
看接口趋势Micrometer + Prometheus + Grafana
做 HTTP 压测k6、JMeter、wrk

不要迷信单一工具。复杂问题通常需要指标、日志、线程栈、JFR 和业务上下文一起看。


🔍 十四、自测问题

text
为什么 P99 比平均响应时间更重要?
CPU 高时如何定位到具体 Java 方法?
线程大量 BLOCKED 通常说明什么?
Full GC 频繁应该收集哪些证据?
为什么压测必须记录错误率?
Arthas trace 为什么不能长时间挂在高频方法上?
JFR 相比普通日志有什么优势?
没有优化前基线,为什么很难证明优化有效?

📌 十五、学习建议

建议准备一个简单 Spring Boot 接口,故意制造三种问题:

text
CPU 密集:循环计算哈希。
内存压力:持续创建大对象。
线程阻塞:sleep 或等待锁。

然后分别使用:

text
jstack 看线程状态
JFR 看 CPU 和分配热点
jstat 看 GC 变化
Arthas trace 看方法耗时

性能工具只有在真实现象中使用过,才能在生产事故中迅速判断该用哪一个。


📌 十六、报告格式

性能分析结论建议写成:

text
现象:
影响范围:
证据:
根因:
修复:
验证:
预防:

不要只写“已优化”。