【Java】性能调优:利用 jstack 寻找 Java 程序卡顿的真相

前言

当 Java 程序出现给人感觉 “卡顿”、“响应慢”、CPU 风调高、系统给予调用总是延迟时,我们需要采用系统层和虚拟机层的合理工具来分析细节。

本文仅从 JVM 的角度来分析,研究如何利用 jstack 进行 Java 程序性能调优。

Java 程序卡顿的常规原因

  1. 线程互换使用不对:

    • 长期卡在 synchronized / ReentrantLock

    • 正处于等待 wait/notify 或者 park/unpark

  2. 线程正处于终端循环,而输入不足。

  3. 线程正处于 I/O 阻塞

    • 如 MySQL 耗时很长

    • Redis 带宽不足

  4. 全局 GC 分时过长,使程序队列拥塑扩大

  5. 正处于微事务阻塞,这在大数据场景中很常见

jstack 能给我们什么?

stack 是 JVM 内部线程状态的环境抽样工具,它能读取正在运行进程所有线程的 call stack。

它可以用于分析:

  • 当前每个线程在做什么

  • 是否有线程太多卡在同一个场景

  • 是否有死锁 / 响应等待

jstack 输出与卡顿分析的对应

jstack 信息分析意义
java.lang.Thread.State: RUNNABLECPU 正在执行的东西
WAITING / TIMED_WAITING线程等待事件,可能卡在锁或等待结果
waiting to lock <0x…>同步类锁,可能有互换错误
locked <0x…>当前线程所拥有的锁
at xxx.yyy.zzz线程标记的当前校验或执行位置

jstack 分析重点指标

  1. 各类线程状态的分布量

     确定是否有大量 WAITING / BLOCKED
    
  2. RUNNABLE 状态中最顶部的几个方法进入频率

     确定是否有 CPU 热点问题
    
  3. waiting to lock 和 locked 的处理

     分析有时是同一个对象导致的失效交互
    
  4. 出现频率指标分析:连续 3 次出现同样的场景即同步热点

重点指标对应检测方法

  1. 各类线程状态的分布量
# grep 线程状态分布
cat dump.jstack.log | grep "java.lang.Thread.State" | sort | uniq -c

  1. 锁卡顿情况
# grep 锁的卡顿情况
cat dump.jstack.log | grep "waiting to lock" | sort | uniq -c | sort -nr
  1. 线程总数
# 线程总数
cat dump.jstack.log | grep '^"' | wc -l

实用脚本

基于以上内容,我让大模型帮我写了一个脚本,能够很方便的得到分析结果

jstack_analyzer.sh

#!/bin/bash

PID=$1
LOOPS=${2:-3}
INTERVAL=${3:-5}

if [ -z "$PID" ]; then
  echo "用法: $0 <pid> [循环次数=3] [间隔秒数=5]"
  exit 1
fi

echo "分析进程 PID: $PID"
echo "采样次数: $LOOPS, 间隔秒数: $INTERVAL"
echo ""

SNAPSHOTS=()
TOP_METHOD_FILES=()

# ============ 采样并记录 =============
for i in $(seq 1 $LOOPS); do
  TS=$(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 ]; then
    sleep "$INTERVAL"
  fi
done

# ============ 常规指标:最后一次快照分析 =============
echo
echo "======================================="
echo "🧭 使用最后一次快照 ${SNAPSHOTS[-1]} 做常规分析"
echo "======================================="

analyze_snapshot() {
  local FILE=$1
  echo "------------ [线程状态统计] ------------"
  grep "java.lang.Thread.State" "$FILE" | sort | uniq -c | sort -nr
  echo

  echo "------------ [RUNNABLE 线程热点栈顶] ------------"
  awk '/java.lang.Thread.State: RUNNABLE/,/^$/' "$FILE" | grep "at " | sort | uniq -c | sort -nr | head -15
  echo

  echo "------------ [锁等待情况 waiting to lock] ------------"
  grep "waiting to lock" "$FILE" | sort | uniq -c | sort -nr | head -10
  echo

  echo "------------ [线程总数] ------------"
  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引起的卡顿,由于线程池满引起的卡顿,由于锁竞争引起的卡顿。未完待续~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值