还在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👏):https://github.com/hengyoush/awesome-java-tools/blob/main/cpu100.sh.
加速链接:https://mirror.ghproxy.com/github.com/hengyoush/awesome-java-tools/blob/main/cpu100.sh

使用方法

./cpu100.sh 1221 3

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

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}",并输出匹配的堆栈
#!/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,以后会不断增强和开发新的工具!

In

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注