前言
当 Java 程序出现给人感觉 “卡顿”、“响应慢”、CPU 风调高、系统给予调用总是延迟时,我们需要采用系统层和虚拟机层的合理工具来分析细节。
本文仅从 JVM 的角度来分析,研究如何利用 jstack 进行 Java 程序性能调优。
Java 程序卡顿的常规原因
-
线程互换使用不对:
-
长期卡在 synchronized / ReentrantLock
-
正处于等待 wait/notify 或者 park/unpark
-
-
线程正处于终端循环,而输入不足。
-
线程正处于 I/O 阻塞
-
如 MySQL 耗时很长
-
Redis 带宽不足
-
-
全局 GC 分时过长,使程序队列拥塑扩大
-
正处于微事务阻塞,这在大数据场景中很常见
jstack 能给我们什么?
stack 是 JVM 内部线程状态的环境抽样工具,它能读取正在运行进程所有线程的 call stack。
它可以用于分析:
-
当前每个线程在做什么
-
是否有线程太多卡在同一个场景
-
是否有死锁 / 响应等待
jstack 输出与卡顿分析的对应
jstack 信息 | 分析意义 |
---|---|
java.lang.Thread.State: RUNNABLE | CPU 正在执行的东西 |
WAITING / TIMED_WAITING | 线程等待事件,可能卡在锁或等待结果 |
waiting to lock <0x…> | 同步类锁,可能有互换错误 |
locked <0x…> | 当前线程所拥有的锁 |
at xxx.yyy.zzz | 线程标记的当前校验或执行位置 |
jstack 分析重点指标
-
各类线程状态的分布量
确定是否有大量 WAITING / BLOCKED
-
RUNNABLE 状态中最顶部的几个方法进入频率
确定是否有 CPU 热点问题
-
waiting to lock 和 locked 的处理
分析有时是同一个对象导致的失效交互
-
出现频率指标分析:连续 3 次出现同样的场景即同步热点
重点指标对应检测方法
- 各类线程状态的分布量
# grep 线程状态分布
cat dump.jstack.log | grep "java.lang.Thread.State" | sort | uniq -c
- 锁卡顿情况
# grep 锁的卡顿情况
cat dump.jstack.log | grep "waiting to lock" | sort | uniq -c | sort -nr
- 线程总数
# 线程总数
cat dump.jstack.log | grep '^"' | wc -l
实用脚本
基于以上内容,我让大模型帮我写了一个脚本,能够很方便的得到分析结果
jstack_analyzer.sh
#!/bin/bashPID=$1
LOOPS=${2:-3}
INTERVAL=${3:-5}if [ -z "$PID" ]; thenecho "用法: $0 <pid> [循环次数=3] [间隔秒数=5]"exit 1
fiecho "分析进程 PID: $PID"
echo "采样次数: $LOOPS, 间隔秒数: $INTERVAL"
echo ""SNAPSHOTS=()
TOP_METHOD_FILES=()# ============ 采样并记录 =============
for i in $(seq 1 $LOOPS); doTS=$(date +%Y%m%d-%H%M%S)FILE="jstack_${PID}_$TS.txt"jstack "$PID" > "$FILE"echo "📸 第 $i 次采样: $FILE"SNAPSHOTS+=("$FILE")# 提取 RUNNABLE 栈顶方法,记录到临时文件TOPFILE="runnable_top_${i}.txt"awk '/java.lang.Thread.State: RUNNABLE/,/^$/' "$FILE" \| grep "^ *at " | head -1 | sed 's/^ *//' >> "$TOPFILE"TOP_METHOD_FILES+=("$TOPFILE")if [ $i -lt $LOOPS ]; thensleep "$INTERVAL"fi
done# ============ 常规指标:最后一次快照分析 =============
echo
echo "======================================="
echo "🧭 使用最后一次快照 ${SNAPSHOTS[-1]} 做常规分析"
echo "======================================="analyze_snapshot() {local FILE=$1echo "------------ [线程状态统计] ------------"grep "java.lang.Thread.State" "$FILE" | sort | uniq -c | sort -nrechoecho "------------ [RUNNABLE 线程热点栈顶] ------------"awk '/java.lang.Thread.State: RUNNABLE/,/^$/' "$FILE" | grep "at " | sort | uniq -c | sort -nr | head -15echoecho "------------ [锁等待情况 waiting to lock] ------------"grep "waiting to lock" "$FILE" | sort | uniq -c | sort -nr | head -10echoecho "------------ [线程总数] ------------"grep -c '^"' "$FILE"echo
}analyze_snapshot "${SNAPSHOTS[-1]}"# ============ 差异化分析:RUNNABLE 栈顶热点交集 ============
echo
echo "======================================="
echo "🔥 持续热点方法分析(${LOOPS} 次采样中都出现)"
echo "======================================="# 合并所有栈顶方法,统计频率
cat "${TOP_METHOD_FILES[@]}" | sort | uniq -c | sort -nr > merged_top.txt# 打印出现 >= 所有轮次 的方法(持续热点)
awk -v threshold="$LOOPS" '$1 >= threshold {print $1, $2, $3, $4, "..."}' merged_top.txt# 可选:清理中间文件
# rm -f runnable_top_*.txt merged_top.txt
使用方法:
chmod u+x jstack_analyzer.sh
./jstack_analyzer.sh 5 10
输出结果:
通过这个结果再结合top来分析(我这是一个正常的程序,没有卡顿)通过top来看负载不高,cpu占用率不高。
于是可以得到结论:
大量 WAITING + 低负载(low CPU) = 当前进程大概率“处于空闲/低活动状态”,这通常是正常的。
等有时间我将会再举出几个卡顿的例子,如由于IO引起的卡顿,由于线程池满引起的卡顿,由于锁竞争引起的卡顿。未完待续~