#!/bin/bash
# =============================================================================
# 脚本名称:show-busy-java-threads.sh
# 功能描述:找出 Java 进程中 CPU 占用最高的线程,并打印这些线程的堆栈信息
# 作者:Jerry Lee
# 源码:https://github.com/oldratlee/useful-shells/blob/master/show-busy-java-threads.sh
# =============================================================================

# 获取脚本名称
PROG=`basename $0`

# -----------------------------------------------------------------------------
# 函数:usage
# 功能:显示帮助信息并退出
# 参数:$1 - 退出状态码
# -----------------------------------------------------------------------------
usage() {
    cat <<EOF
Usage: ${PROG} [OPTION]...
Find out the highest cpu consumed threads of java, and print the stack of these threads.
Example: ${PROG} -c 10

Options:
    -p, --pid       find out the highest cpu consumed threads from the specifed java process,
                    default from all java process.
    -c, --count     set the thread count to show, default is 5
    -h, --help      display this help and exit
EOF
    exit $1
}

# -----------------------------------------------------------------------------
# 解析命令行参数
# 使用 getopt 进行参数解析,支持短选项和长选项
# -----------------------------------------------------------------------------
ARGS=`getopt -n "$PROG" -a -o c:p:h -l count:,pid:,help -- "$@"`
[ $? -ne 0 ] && usage 1
eval set -- "${ARGS}"

# 遍历解析后的参数
while true; do
    case "$1" in
    -c|--count)
        count="$2"
        shift 2
        ;;
    -p|--pid)
        pid="$2"
        shift 2
        ;;
    -h|--help)
        usage
        ;;
    --)
        shift
        break
        ;;
    esac
done

# 设置默认值:显示前 5 个线程
count=${count:-5}

# -----------------------------------------------------------------------------
# 函数:redEcho
# 功能:以红色字体输出文本(如果 stdout 是终端)
# 参数:$@ - 要输出的文本
# -----------------------------------------------------------------------------
redEcho() {
    [ -c /dev/stdout ] && {
        # 如果 stdout 是控制台,启用彩色输出
        echo -ne "\033[1;31m"
        echo -n "$@"
        echo -e "\033[0m"
    } || echo "$@"
}

# -----------------------------------------------------------------------------
# 检查 jstack 命令是否存在
# jstack 是 JDK 提供的线程堆栈查看工具
# -----------------------------------------------------------------------------
if ! which jstack &> /dev/null; then
    [ -z "$JAVA_HOME" ] && {
        redEcho "Error: jstack not found on PATH!"
        exit 1
    }
    ! [ -f "$JAVA_HOME/bin/jstack" ] && {
        redEcho "Error: jstack not found on PATH and $JAVA_HOME/bin/jstack file does NOT exists!"
        exit 1
    }
    ! [ -x "$JAVA_HOME/bin/jstack" ] && {
        redEcho "Error: jstack not found on PATH and $JAVA_HOME/bin/jstack is NOT executalbe!"
        exit 1
    }
    export PATH="$JAVA_HOME/bin:$PATH"
fi

# 生成唯一标识符,用于临时文件命名
# 格式:时间戳_随机数_进程 ID
uuid=`date +%s`_${RANDOM}_$$

# -----------------------------------------------------------------------------
# 函数:cleanupWhenExit
# 功能:脚本退出时清理临时文件
# -----------------------------------------------------------------------------
cleanupWhenExit() {
    rm /tmp/${uuid}_* &> /dev/null
}
# 注册退出时的清理操作
trap "cleanupWhenExit" EXIT

# -----------------------------------------------------------------------------
# 函数:printStackOfThread
# 功能:打印指定线程的堆栈信息
# 输入:通过管道读取线程信息(格式:pid threadId user pcpu)
# -----------------------------------------------------------------------------
printStackOfThread() {
    while read threadLine ; do
        # 解析线程信息
        pid=`echo ${threadLine} | awk '{print $1}'`
        threadId=`echo ${threadLine} | awk '{print $2}'`
        # 将线程 ID 转换为十六进制(jstack 输出中使用十六进制)
        threadId0x=`printf %x ${threadId}`
        user=`echo ${threadLine} | awk '{print $3}'`
        pcpu=`echo ${threadLine} | awk '{print $5}'`
        
        # 定义 jstack 输出文件路径
        jstackFile=/tmp/${uuid}_${pid}
        
        # 如果还没有获取该进程的堆栈,则执行 jstack
        [ ! -f "${jstackFile}" ] && {
            jstack ${pid} > ${jstackFile} || {
                redEcho "Fail to jstack java process ${pid}!"
                rm ${jstackFile}
                continue
            }
        }

        # 输出线程堆栈信息
        redEcho "Busy(${pcpu}%) thread(${threadId}/0x${threadId0x}) stack of java process(${pid}) under user(${user}):"
        # 从 jstack 输出中提取指定线程的堆栈(从 nid=0x... 开始到空行结束)
        sed "/nid=0x${threadId0x}/,/^$/p" -n ${jstackFile}
    done
}

# -----------------------------------------------------------------------------
# 主流程:
# 1. 使用 ps 命令获取所有 Java 线程的 CPU 使用率
# 2. 根据是否指定 pid 进行过滤
# 3. 按 CPU 使用率降序排序
# 4. 取前 N 个线程
# 5. 打印这些线程的堆栈信息
# -----------------------------------------------------------------------------
ps -Leo pid,lwp,user,comm,pcpu --no-headers | {
    # 如果没有指定 pid,获取所有 java 进程;否则获取指定 pid 的 java 进程
    [ -z "${pid}" ] &&
    awk '$4=="java"{print $0}' ||
    awk -v "pid=${pid}" '$1==pid,$4=="java"{print $0}'
} | sort -k5 -r -n | head --lines "${count}" | printStackOfThread

📖 使用示例

1️⃣ 基础用法(显示 CPU 占用最高的 5 个线程)

./show-busy-java-threads.sh

输出示例:

Busy(45.2%) thread(12345/0x3039) stack of java process(6789) under user(root):
"main" #1 prio=5 os_prio=0 tid=0x00007f8b8c001000 nid=0x3039 runnable [0x00007f8b94a3e000]
   java.lang.Thread.State: RUNNABLE
        at com.example.MyClass.myMethod(MyClass.java:123)
        at com.example.MyClass.anotherMethod(MyClass.java:456)
        ...

Busy(32.1%) thread(12346/0x303a) stack of java process(6789) under user(root):
"http-nio-8080-exec-1" #10 prio=5 os_prio=0 tid=0x00007f8b8c002000 nid=0x303a runnable [0x00007f8b94b3f000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        ...

2️⃣ 显示前 10 个线程

./show-busy-java-threads.sh -c 10
# 或使用长选项
./show-busy-java-threads.sh --count 10

3️⃣ 查看指定 Java 进程的线程

# 先找到 Java 进程的 PID
ps aux | grep java

# 假设 PID 是 12345
./show-busy-java-threads.sh -p 12345
# 或使用长选项
./show-busy-java-threads.sh --pid 12345

4️⃣ 组合使用

# 查看 PID 为 12345 的 Java 进程中 CPU 占用最高的 20 个线程
./show-busy-java-threads.sh -p 12345 -c 20

5️⃣ 查看帮助信息

./show-busy-java-threads.sh -h
# 或
./show-busy-java-threads.sh --help

帮助输出:

Usage: show-busy-java-threads.sh [OPTION]...
Find out the highest cpu consumed threads of java, and print the stack of these threads.
Example: show-busy-java-threads.sh -c 10

Options:
    -p, --pid       find out the highest cpu consumed threads from the specifed java process,
                    default from all java process.
    -c, --count     set the thread count to show, default is 5
    -h, --help      display this help and exit

🔍 实际应用场景

场景 1:生产环境 CPU 100% 问题排查

# 直接运行,查看哪个 Java 线程占用 CPU 最高
./show-busy-java-threads.sh

# 如果发现是 GC 线程,可能是内存问题
# 如果发现是业务线程,可以定位到具体代码行

场景 2:特定应用性能分析

# 先找到目标应用的 PID
jps -l

# 分析该应用的线程
./show-busy-java-threads.sh -p <PID> -c 15

场景 3:定期监控(配合 watch 命令)

# 每 2 秒刷新一次
watch -n 2 './show-busy-java-threads.sh -c 3'

github: https://github.com/yikebocai/shell/blob/master/show-busy-java-threads.sh

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐