avatar

Ryan's Blog

The first step is always the hardest.

  • 首页
  • 分类
  • 标签
  • 归档
  • 关于
  • 工具
Home Java CPU 高负载排查实战:top -H、jstack、jcmd 与线程栈分析
文章

Java CPU 高负载排查实战:top -H、jstack、jcmd 与线程栈分析

Posted 2023-02-23 Updated 9 days ago
By Ryan Chen
32~42 min read

前言

Java 服务出现 CPU 高负载时,最常见的排查思路是:先找到 CPU 占用高的 Java 进程,再找到该进程里 CPU 占用高的线程,最后把 Linux 线程 ID 转成 JVM 线程栈里的 nid,通过线程栈判断是哪段代码在消耗 CPU。

这篇文章整理一套可复用的 Java CPU 高负载排查 SOP,覆盖:

  • top 找高 CPU Java 进程;
  • top -H 或 ps -L 找高 CPU 线程;
  • 线程 ID 转 16 进制;
  • jstack -l 和 jcmd Thread.print -l 抓线程栈;
  • 如何判断线程栈是否真的异常;
  • Netty EventLoop、GC、死循环、正则回溯等常见场景;
  • 容器环境下的注意事项;
  • 一键采样脚本模板。

如果你需要更多 Linux 排查命令,可以参考:Linux 常用命令速查:磁盘、日志、进程、网络、Docker 与文件排查实践。如果 CPU 高和系统参数、连接数、队列有关,也可以参考:Linux 内核与网络参数调优:sysctl、文件描述符、TCP 队列与高并发配置。

jstack 和 jcmd 能解决什么问题

jstack 是 JDK 自带的线程栈诊断工具,可以输出 Java 进程当前时刻的线程快照,包括线程名、线程状态、nid、调用栈和锁信息。

线程快照适合排查:

问题 说明
CPU 高 找到持续 RUNNABLE 且堆栈稳定停留在热点代码的线程。
死锁 查看线程互相持有和等待锁的情况。
线程阻塞 观察大量线程是否卡在锁、IO、数据库、HTTP 调用等位置。
线程池耗尽 观察业务线程池是否堆积在同类调用栈。
Netty / Tomcat / Dubbo 等框架问题 观察 IO 线程、业务线程、调度线程的状态。

新版本 JDK 中,jcmd 是更统一的诊断入口,可以使用:

jcmd <pid> Thread.print -l

传统方式则是:

jstack -l <pid>

两者都能用于线程栈分析。线上排查时,建议优先使用当前 JDK 自带工具,并确认执行用户和 Java 进程用户一致。

排查前准备

建议先确认以下信息:

# Java 进程
jps -l
ps -ef | grep java

# JDK 工具是否存在
which jstack
which jcmd
java -version

# 系统负载
uptime
top

注意事项:

  1. 执行 jstack / jcmd 的用户最好和 Java 进程用户一致;
  2. 如果是容器环境,要进入容器内部或确认宿主机能看到目标 PID;
  3. CPU 高问题通常需要连续采样,单次线程栈容易误判;
  4. 线程栈可能包含业务类名、SQL、URL、内部包名,外发前要脱敏。

步骤一:用 top 找 CPU 高的 Java 进程

输入命令:

top

示例输出:

Tasks:   3 total,   1 running,   2 sleeping,   0 stopped,   0 zombie
%Cpu(s):  2.1 us,  1.1 sy,  0.0 ni, 93.7 id,  2.9 wa,  0.0 hi,  0.3 si,  0.0 st
KiB Mem : 32675864 total, 10846008 free,  2076000 used, 19753856 buff/cache
KiB Swap:        0 total,        0 free,        0 used. 30297432 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
      1 root      20   0 2705876 216868  13292 S   7.6  0.7  65:31.27 java
   1238 root      20   0   15120   3440   3060 S   0.0  0.0   0:00.01 bash
   1252 root      20   0   59488   4404   3788 R   0.0  0.0   0:00.00 top

这里 Java 进程 PID 是 1。

如果有多个 Java 进程,可以用:

jps -l
ps -eo pid,ppid,user,%cpu,%mem,cmd --sort=-%cpu | head

步骤二:用 top -H 找 CPU 高的线程

查看某个进程内的线程:

top -H -p 1

示例输出:

top - 09:44:41 up 8 days, 14 min,  0 users,  load average: 0.65, 0.82, 0.99
Threads: 110 total,   0 running, 110 sleeping,   0 stopped,   0 zombie
%Cpu(s):  2.5 us,  1.6 sy,  0.0 ni, 94.3 id,  1.6 wa,  0.0 hi,  0.0 si,  0.0 st

    PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
     58 root      20   0 2705876 216868  13292 S  6.7  0.7   4:11.22 java
     59 root      20   0 2705876 216868  13292 S  6.7  0.7   6:40.24 java
    111 root      20   0 2705876 216868  13292 S  6.7  0.7   6:03.95 java

这里 58、59、111 是 Linux 线程 ID,也就是后面要在 JVM 线程栈中匹配的 nid。

也可以用批处理方式查看线程:

top -H -b -n 1 -p 1 | head -n 30

或者使用 ps:

ps -L -p 1 -o pid,tid,pcpu,pmem,comm --sort=-pcpu | head

其中 tid 就是线程 ID。

步骤三:线程 ID 转十六进制 nid

JVM 线程栈中的本地线程 ID 使用十六进制显示,例如 nid=0x3a。

将 Linux 线程 ID 转为十六进制:

printf "0x%x\n" 58

输出:

0x3a

如果要批量转换:

for tid in 58 59 111; do
  printf "%s -> 0x%x\n" "$tid" "$tid"
done

输出示例:

58 -> 0x3a
59 -> 0x3b
111 -> 0x6f

步骤四:用 jstack 定位线程栈

抓取线程栈:

jstack -l 1 > jstack-$(date +%F-%H%M%S).log

查找对应 nid:

grep -A 50 "nid=0x3a" jstack-*.log

也可以直接:

jstack -l 1 | grep -A 50 "nid=0x3a"

示例:

"nioEventLoopGroup-4-1" #48 prio=10 os_prio=0 tid=0x5b320000 nid=0x3a runnable [0x56e5b000]
   java.lang.Thread.State: RUNNABLE
        at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
        at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269)
        at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93)
        at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
        - locked <0x68c05ab8> (a io.netty.channel.nio.SelectedSelectionKeySet)
        - locked <0x68c05ac8> (a java.util.Collections$UnmodifiableSet)
        - locked <0x68c05a78> (a sun.nio.ch.EPollSelectorImpl)
        at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
        at io.netty.channel.nio.NioEventLoop.select(NioEventLoop.java:805)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:457)
        at java.lang.Thread.run(Thread.java:748)

这里要特别注意:看到 nid=0x3a 只能说明找到了对应线程,不能直接说明它就是问题根因。

例如上面的 Netty EventLoop 停在 epollWait,很多时候是在等待 IO 事件,可能是正常状态。是否异常,需要结合连续采样、CPU 时间变化和业务现象判断。

步骤五:用 jcmd Thread.print 抓线程栈

新版本 JDK 推荐使用 jcmd 作为统一诊断入口:

jcmd 1 Thread.print -l > thread-$(date +%F-%H%M%S).log

查找对应线程:

grep -A 50 "nid=0x3a" thread-*.log

jcmd 还可以查看该进程支持的诊断命令:

jcmd 1 help

常见对比:

工具 用途
jstack -l <pid> 经典线程栈工具,适合快速抓取 Java 线程快照。
jcmd <pid> Thread.print -l 新版本 JDK 常用诊断入口,也能输出线程栈和锁信息。
jcmd <pid> GC.heap_info 查看堆信息,辅助判断是否 GC 压力导致 CPU 高。
jcmd <pid> JFR.start 开启 JFR,适合持续性能问题分析。

为什么要连续采样

CPU 高负载问题常常是动态的。单次线程栈可能刚好抓到线程处于 IO 等待、锁等待或正常调度点,容易误判。

建议连续抓 3~5 次,每次间隔 5~10 秒:

PID=1
for i in 1 2 3 4 5; do
  now=$(date +%F-%H%M%S)
  top -H -b -n 1 -p $PID | head -n 40 > top-$now.log
  jcmd $PID Thread.print -l > thread-$now.log
  sleep 5
done

判断原则:

  1. 同一个线程 ID 是否持续出现在 CPU 排名前列;
  2. 同一个 nid 的线程栈是否多次停留在同一段业务代码;
  3. 线程状态是否持续为 RUNNABLE;
  4. CPU 时间是否持续增长;
  5. 是否和业务流量、慢 SQL、GC、外部调用异常同一时间出现。

如何判断线程栈是否异常

常见可疑堆栈

堆栈特征 可能原因
持续停留在业务循环代码 死循环、循环条件错误、集合遍历过大。
持续停留在正则相关方法 正则回溯严重。
持续停留在 JSON 序列化 / 反序列化 大对象、循环引用、字段过多。
持续停留在加解密 / 压缩 / 编码 CPU 密集型任务。
大量线程争抢同一锁 锁竞争、同步块过大。
GC 线程 CPU 高 可能是内存压力、频繁 Full GC。
业务线程持续 RUNNABLE 且栈不变 高概率热点代码或死循环。

不一定异常的堆栈

堆栈 说明
epollWait / socketRead 可能是正常 IO 等待,要结合 CPU 时间判断。
Unsafe.park / LockSupport.park 线程池空闲或等待任务时很常见。
Object.wait 等待条件或队列消息,不一定是问题。
Thread.sleep 定时任务或重试等待中常见。

所以排查时不能只看一次 RUNNABLE,更要看“是否持续消耗 CPU”。

常见高 CPU 场景

1. 死循环

典型特征:同一线程持续高 CPU,堆栈稳定停在某个业务方法或循环逻辑。

处理建议:

  • 检查循环退出条件;
  • 检查数据是否异常导致循环无法结束;
  • 增加最大迭代次数或保护条件;
  • 补充单元测试覆盖边界条件。

2. 正则回溯

典型特征:堆栈停留在 java.util.regex.Pattern 相关方法。

处理建议:

  • 避免灾难性回溯的正则;
  • 对输入长度做限制;
  • 使用更明确的匹配规则;
  • 对高风险正则做压测。

3. JSON / XML 大对象处理

典型特征:堆栈停留在 Jackson、Fastjson、Gson、JAXB 等序列化框架。

处理建议:

  • 控制对象大小;
  • 避免循环引用;
  • 减少无用字段;
  • 对大对象处理做异步化或分页。

4. GC 导致 CPU 高

如果多个 GC 线程占用 CPU,或者业务线程频繁分配对象,需要结合 GC 日志和堆信息判断。

可以执行:

jcmd 1 GC.heap_info
jcmd 1 GC.class_histogram | head -n 30

也可以查看 GC 日志、JFR 或监控平台中的 Young GC / Full GC 次数和耗时。

5. 线程池任务堆积

如果大量业务线程堆栈类似,可能是线程池中任务执行慢、下游接口慢、数据库慢或锁竞争。

建议结合:

  • 线程池活跃线程数;
  • 队列长度;
  • 拒绝次数;
  • 下游接口耗时;
  • 慢 SQL;
  • 日志中的 timeout / retry。

容器环境注意事项

容器中排查 Java CPU 高有几个坑:

  1. 容器内 PID 可能是 1,宿主机看到的是另一个 PID;
  2. top -H -p <pid> 要在能看到目标线程的命名空间里执行;
  3. 容器镜像可能只有 JRE,没有 jstack / jcmd;
  4. 如果工具不在容器里,可以使用带 JDK 的 debug 镜像或进入宿主机排查;
  5. Kubernetes 中要注意 CPU limit,容器被限流时表现可能和宿主机 CPU 高不同。

进入容器后排查:

docker exec -it <container> bash
jps -l
top -H -p 1
jcmd 1 Thread.print -l

Kubernetes 场景:

kubectl exec -it <pod> -- bash
kubectl top pod <pod>

如果镜像不包含诊断工具,建议在构建基础镜像时保留必要的 JDK 工具,或者准备专门的 debug 方案。

一键采样脚本

下面脚本适合快速保存现场:

#!/usr/bin/env bash
set -euo pipefail

PID=${1:?"Usage: $0 <java-pid>"}
OUT_DIR="cpu-dump-${PID}-$(date +%F-%H%M%S)"
mkdir -p "$OUT_DIR"

for i in 1 2 3 4 5; do
  now=$(date +%F-%H%M%S)
  echo "sampling $i at $now"

  top -H -b -n 1 -p "$PID" | head -n 50 > "$OUT_DIR/top-$now.log"

  if command -v jcmd >/dev/null 2>&1; then
    jcmd "$PID" Thread.print -l > "$OUT_DIR/thread-$now.log" || true
    jcmd "$PID" GC.heap_info > "$OUT_DIR/heap-$now.log" || true
  elif command -v jstack >/dev/null 2>&1; then
    jstack -l "$PID" > "$OUT_DIR/thread-$now.log" || true
  else
    echo "jcmd/jstack not found" > "$OUT_DIR/error-$now.log"
  fi

  sleep 5
done

tar -czf "$OUT_DIR.tar.gz" "$OUT_DIR"
echo "saved: $OUT_DIR.tar.gz"

使用方式:

bash collect-java-cpu.sh 1

采样完成后,重点对比每次 top-*.log 中 CPU 高的线程 ID,再到 thread-*.log 里找对应 nid=0x...。

排查流程总结

Java CPU 高负载排查可以按下面流程执行:

1. top 找 CPU 高的 Java 进程 PID
2. top -H -p PID 找 CPU 高的线程 TID
3. printf "0x%x\n" TID 转成十六进制 nid
4. jstack -l PID 或 jcmd PID Thread.print -l 抓线程栈
5. grep "nid=0x..." 定位线程
6. 连续采样 3~5 次
7. 判断线程栈是否稳定停在业务热点代码
8. 结合 GC、日志、慢 SQL、下游调用和监控确认根因

最重要的一点是:找到线程不等于找到根因。必须结合连续采样和业务上下文判断线程是否真的持续消耗 CPU。

参考资料

  • Oracle JDK 8 jstack 工具文档
  • Oracle JDK 8 jcmd 工具说明
  • Oracle JDK 17 Troubleshooting Guide:Diagnostic Tools
  • man7:top(1)
  • man7:ps(1)
  • Linux 常用命令速查:磁盘、日志、进程、网络、Docker 与文件排查实践
  • Linux 内核与网络参数调优:sysctl、文件描述符、TCP 队列与高并发配置
Java
Java JVM JStack jcmd CPU高负载 线程栈 性能调优 线上排查
License:  CC BY 4.0
Share

Further Reading

Sep 12, 2024

RocketMQ 架构设计与应用最佳实践:高可用消息队列核心解析

本文基于 RocketMQ 4.x 经典架构,梳理 NameServer、Broker、Producer、Consumer、Remoting 与 Store 模块,结合消息轨迹、存储模型、FastFail、事务消息和高可用部署,总结高并发场景下的实践要点。

Aug 16, 2023

Java List 核心数据结构解析:ArrayList、LinkedList 与线程安全

系统梳理 Java List 接口、ArrayList 动态数组、LinkedList 双向链表、容量扩容、遍历与 fail-fast 机制,并对比 synchronizedList、CopyOnWriteArrayList、Vector 等线程安全方案的适用场景。

Apr 24, 2023

数组基础详解:概念、存储结构与常用操作

从数组的连续存储、下标访问、Java 数组对象、一维与多维数组、遍历、查找、插入、删除、复制、排序和 Arrays 工具类出发,系统梳理数据结构学习中的数组基础。

OLDER

Spring Cloud OpenFeign 集成 OkHttp 与 Apache HttpClient 5:连接池、超时与性能优化实践

NEWER

SysBench 压测实践:CPU、内存、FileIO 与 MySQL OLTP 基准测试指南

Recently Updated

  • Agent 架构设计原则:Router、Runtime 与 Business Script 的职责划分
  • RocketMQ 架构设计与应用最佳实践:高可用消息队列核心解析
  • Redis 核心概念、数据结构与高可用架构详解
  • B+树原理与 MySQL InnoDB 索引机制解析
  • MySQL AUTO_INCREMENT 插入 0 变成自增值的原因与解决方案

Trending Tags

RocketMQ Windows Feign Docker Zipkin SonarQube OkHttp HttpClient API 性能优化

Contents

©2026 Ryan's Blog. Some rights reserved. · 粤ICP备2022031588号