应直接通过线程栈和系统指标定位性能瓶颈:runnable且cpu飙升为cpu密集型;waiting/timed_waiting多为i/o阻塞;blocked表明锁竞争;虚拟线程需用jfr而非jstack分析。

直接看线程栈和系统指标,别猜。慢请求卡在 I/O 还是 CPU,本质是线程处于 BLOCKED / WAITING 状态(等 I/O),还是 RUNNABLE 状态且 CPU 使用率同步飙升(真计算)。
用 jstack 抓线程状态,重点看 java.lang.Thread.State
执行 jstack -l <pid></pid>,过滤出慢请求对应线程(比如通过 traceId、URL 路径或线程名定位),观察其状态:
-
java.lang.Thread.State: RUNNABLE且堆栈停留在String.hashCode、ArrayList.grow、复杂正则匹配、JSON 序列化深层嵌套对象等——大概率是 CPU 密集型,正在干算力活 -
java.lang.Thread.State: WAITING (on object monitor)或java.lang.Thread.State: TIMED_WAITING,堆栈里有SocketInputStream.read、SSLSocketImpl.read、JdbcConnection.await、HttpClient.sendAsync——基本确定卡在 I/O 等待上 -
java.lang.Thread.State: BLOCKED (on object monitor),堆栈末尾显示waiting to lock——不是 I/O 也不是 CPU,是锁竞争,另起一摊事
配合 top -Hp <pid></pid> + printf "%x\n" <tid></tid> 定位高 CPU 线程
如果怀疑是 CPU 问题,不能只信 jstack 里看到的 RUNNABLE:它可能刚执行完一行就切走了,实际不耗时。要实锤,得结合操作系统级观测:
- 用
top -Hp <pid></pid>找出 CPU 占用最高的线程 PID(十进制) - 转成十六进制:
printf "%x\n" <tid></tid> - 回到
jstack输出里搜索这个十六进制值,找到对应线程栈 - 如果该栈确实深陷循环、递归、大对象拷贝或 GC 相关(如
ParallelScavengeHeap.collect),才坐实 CPU 问题
查系统 %iowait 和磁盘 await,验证 I/O 是否真瓶颈
单看 Java 线程栈还不够,得确认底层设备是否真的拖后腿:
- 运行
mpstat -P ALL 1 3,盯住每核的%iowait:持续 >10% 就值得怀疑 - 运行
iostat -x 1 3,重点关注await(平均 I/O 延迟)和%util(设备忙时比):如果await > 10ms且%util ≈ 100%,说明磁盘已饱和 - 注意:Java 应用大量刷日志(尤其同步
FileAppender)、频繁小文件读写、未配置连接池的 JDBC 直连 DB,都容易把await拉高
别漏掉虚拟线程场景下的“假等待”
如果你用的是 OpenJDK 21+ 并启用了虚拟线程(-XX:+EnablePreview -Djdk.virtualThreadScheduler.parallelism=... ),传统 jstack 看不到虚拟线程栈,所有 IO 等待都会“消失”在线程池载体上。此时:
-
jstack显示载体线程处于RUNNABLE,但实际在做 park/unpark 调度,不代表它在消耗 CPU - 必须用
jcmd <pid> VM.native_memory summary</pid>或jfr start --duration=30s录制飞行记录,再用 JDK Mission Control 查看jdk.VirtualThreadParked事件 - 否则你会误判:明明是 I/O 等待,却因为虚拟线程卸载机制,让线程看起来“很忙”,而 CPU 却不高
最常被忽略的是:I/O 等待本身不耗 CPU,但线程数过多导致调度开销、GC 压力上升、日志刷盘阻塞,这些副作用会让 %iowait 和 CPU us/sy 同时异常。所以永远别只盯一个指标。











