不要再手动jstack了, 用这个工具只需1秒找到在疯狂消耗CPU的Java堆栈

背景

在排查Java进程CPU使用率过高的时候你是否和我有相似的经历?

🤔 忘记排查步骤只记得要转换什么东西到16进制

😡 忘记top命令参数然后被某SDN的复制粘贴文章搞到崩溃

😩 在搜索引擎上搜索"在线转换16进制"甚至使用计算器

😓 没来及排查完就同事就重启了进程还被他阴阳怪气排查问题速度慢

以往我们排查CPU打满的步骤是这样的:

  1. top -Hp {pid}: 查看该Java进程内所有线程的资源占用情况
  2. 将pid转换为16进制,Linux高手可以用:printf "%x\n"{pid}打印出线程id的16进制
  3. jstack -l <pid> > jstack.txt:获取此时的所有线程快照并输入到文件中
  4. 查找文件内容包含nid={16进制id}的线程的堆栈

原理其实很简单,但是步骤要4步,速度快的人恐怕也需要2min左右才能做完,而且每次只能查看一个线程,要排查多个线程需要重复以上步骤花费大量时间,所以我写了一个shell脚本帮助你快速搞定这一切.

github链接下载(欢迎star👏):github.com/hengyoush/J....

加速链接:mirror.ghproxy.com/github.com/...

使用方法

bash 复制代码
./cpu100.sh 1221 3

如上命令输出制定进程Id 1221下cpu使用率最高的3个线程堆栈,输出如下:

bash 复制代码
Thread ID: 11777(0x2e01), CPU Usage: 5.9%

"Cat-RealtimeConsumer-ProblemAnalyzer-16-0" #5935 daemon prio=5 os_prio=0 cpu=33301.45ms elapsed=2423.07s tid=0x00007ff1bc75fbd0 nid=0x2e01 runnable  [0x00007ff0e5645000]
9221
   java.lang.Thread.State: TIMED_WAITING (parking)
        at jdk.internal.misc.Unsafe.park(java.base@17.0.6/Native Method)
        - parking to wait for  <0x00000005ffdbd1b0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.parkNanos(java.base@17.0.6/LockSupport.java:252)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(java.base@17.0.6/AbstractQueuedSynchronizer.java:1672)
        at java.util.concurrent.ArrayBlockingQueue.poll(java.base@17.0.6/ArrayBlockingQueue.java:435)
        at com.dianping.cat.message.io.DefaultMessageQueue.poll(DefaultMessageQueue.java:59)
        at com.dianping.cat.analysis.AbstractMessageAnalyzer.analyze(AbstractMessageAnalyzer.java:62)
        at com.dianping.cat.analysis.PeriodTask.run(PeriodTask.java:116)
        at java.lang.Thread.run(java.base@17.0.6/Thread.java:833)
        at org.unidal.helper.Threads$RunnableThread.run(Threads.java:294)

   Locked ownable synchronizers:
        - None

---------------------------------------------
Thread ID: 11789(0x2e0d), CPU Usage: 5.9%

"Cat-RealtimeConsumer-StateAnalyzer-16-0" #5947 daemon prio=5 os_prio=0 cpu=7448.45ms elapsed=2423.05s tid=0x00007ff1bc309800 nid=0x2e0d runnable  [0x00007ff16498d000]
9413
   java.lang.Thread.State: TIMED_WAITING (parking)
        at jdk.internal.misc.Unsafe.park(java.base@17.0.6/Native Method)
        - parking to wait for  <0x00000005ffea5da8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.parkNanos(java.base@17.0.6/LockSupport.java:252)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(java.base@17.0.6/AbstractQueuedSynchronizer.java:1672)
        at java.util.concurrent.ArrayBlockingQueue.poll(java.base@17.0.6/ArrayBlockingQueue.java:435)
        at com.dianping.cat.message.io.DefaultMessageQueue.poll(DefaultMessageQueue.java:59)
        at com.dianping.cat.analysis.AbstractMessageAnalyzer.analyze(AbstractMessageAnalyzer.java:62)
        at com.dianping.cat.analysis.PeriodTask.run(PeriodTask.java:116)
        at java.lang.Thread.run(java.base@17.0.6/Thread.java:833)
        at org.unidal.helper.Threads$RunnableThread.run(Threads.java:294)

   Locked ownable synchronizers:
        - None

工具源码

工具源码如下,后续可能会有更新,最新的可以关注github仓库。

这个工具实际上就是上述4个步骤的结合:

  1. 首先jstack获取堆栈
  2. 然后找到进程下CPU使用率最高的前N个线程的ID
  3. 在jstack输出中匹配"nid={16进制线程id}",并输出匹配的堆栈
bash 复制代码
#!/bin/bash

if [ $# -ne 2 ]; then
    echo "Usage: $0 <Java_PID> <Number of Threads>"
    exit 1
fi

java_pid=$1
n=$2
user=$(ps -o user= -p $java_pid | awk '{print $1}')
# 1. 获取进程的jstack线程堆栈
jstack_output=$(sudo -u $user jstack -l $java_pid)

# 2. 找出进程下CPU使用率最高的前N个线程的ID

top_threads_info=$(top -b -n 1 -H -p $java_pid | awk 'NR>7 && $9 != "0.0" {print $1, $9}' | head -n "$n" )

# 3. 在jstack输出中匹配"nid={16进制线程id}",并输出匹配的堆栈
echo "Top $n CPU-consuming threads for Java PID $java_pid (in hexadecimal):"
while read -r thread_id cpu_usage; do
  hex_thread_id=$(printf "0x%x" "$thread_id")
  echo "Thread ID: $thread_id($hex_thread_id), CPU Usage: $cpu_usage%"
  echo ""
  awk -v hex_id="$hex_thread_id" '
         BEGIN { flag=0 ;start=0 }
         { lines[NR] = $0 }
         $0 ~ "nid=" hex_id { print; flag=1;start=NR;print start }
         flag && /^"/ && start!=NR { if (flag) exit }
         END { for (i=start; i<=NR; i++) if (flag && lines[i] !~ /^"/) print lines[i] }
       ' <<< "$jstack_output"
  echo "---------------------------------------------"
done <<< "$top_threads_info"

解释下最后匹配堆栈的部分,目的是找到包含特定线程ID的相关信息,并输出该目标线程及其下面的信息直到下一个线程堆栈。

  1. BEGIN { flag=0; start=0 }:在处理开始之前,初始化两个变量,flag 用于标记是否找到了匹配的线程,start 用于记录找到匹配线程的行号。

  2. { lines[NR] = $0 }:对每一行进行处理时,将当前行保存在名为 lines 的数组中,数组的索引是当前行的行号 NR

  3. $0 ~ "nid=" hex_id { print; flag=1; start=NR; print start }:如果当前行包含与给定 hex_thread_id 相匹配的线程ID,则打印当前行,将 flag 设置为1,将 start 设置为当前行的行号。

  4. flag && /^"/ && start!=NR { if (flag) exit }:如果 flag 为1,当前行以双引号开头,并且 start 不等于当前行的行号,则退出 awk 脚本。这是为了避免提前退出,确保在找到匹配线程后继续处理直到遇到新的以双引号开头的行。

  5. END { for (i=start; i<=NR; i++) if (flag && lines[i] !~ /^"/) print lines[i] }:在处理结束后,从 start 行开始,输出匹配线程及其下面的行,直到遇到新的以双引号开头的行。

总结

如上,一键就可以看到当前CPU使用率最高的堆栈了。

觉得它有用吗,不妨尝试下,我的仓库里还有另外一个内存泄漏排查工具,也可以看看哦。不妨关注一下我的github,以后会不断增强和开发新的工具!

相关推荐
Zane199432 分钟前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端
杨充1 小时前
11.DDD与战术建模
设计模式·开源·代码规范
杨充1 小时前
12.综合实战图片框架
设计模式·开源·代码规范
噢,我明白了1 小时前
java中Excel的导入和导出(EasyExcel)
java·开发语言·excel
吠品1 小时前
Zabbix Web界面误报Server未运行的排查与解决
java·服务器·数据库
啊湘1 小时前
天气查询API接口 按月Token鉴权 实时天气 物联网可用 文档齐全
java·后端·struts
Java内核笔记2 小时前
告别十亿美元的错误 : Spring Boot 4 空安全 (JSpecify) 实战
java·spring boot·后端
DisonTangor2 小时前
【微软开源】Mage-Flow 深度解读:微软4B参数图像生成与编辑模型,单卡A100仅需0.59秒
人工智能·microsoft·ai作画·开源·aigc
糖果店的幽灵2 小时前
langgraph的 MessagesState 解读
java·开发语言·人工智能·windows·langgraph
极光代码工作室3 小时前
基于SpringBoot的在线博客系统
java·springboot·web开发·后端开发