Skip to content

Java 服务 CPU 飙升 100%

01 前置理论与核心机制

1. 为什么不能直接重启?

  • 保留现场:CPU 飙升通常由特定流量触发的死循环、死锁或 GC 引起。如果直接重启,内存和线程现场会丢失,异常流量再次进入时服务仍会宕机,导致无法定位根因。
  • 排查时机:只要进程还在运行,可以通过 Linux 原生命令链路(top -> top -Hp -> jstack),在几分钟内从进程级定位到具体的 Java 源码行号。

2. Linux 线程模型与 Java 线程的映射关系

  • LWP(轻量级进程):在 Linux 系统中,Java 的每一个 Thread 实际映射为一个内核级的 LWP。因此,操作系统能直接监控每个 Java 线程的 CPU 开销。
  • 进制差异转换:Linux 层面(如 top 命令)展示的线程 ID(PID/TID)是十进制;而 JVM 内部通过 jstack 导出的线程快照(Thread Dump)中,nid (Native ID) 是十六进制。排查时需进行进制转换。

02 故障排查与恢复 SOP

在服务 CPU 飙升、接口阻塞的情况下,按“进程 → 线程 → 十六进制 → 堆栈”链路执行:

步骤一:找出占用 CPU 最高的 Java 进程

# 查看系统全局负载,按大写 P 按照 CPU 占用率倒序排列
top
  • 观测点:找到 CPU 占用率(%CPU)最高的那一行,确认是否为 Java 进程,记下最左侧的进程号 (例如 11119)。

步骤二:找出进程内占用 CPU 最高的线程

由于一个进程内包含大量线程,需进一步锁定高耗能线程:

# 查看指定进程内部的所有线程资源开销
top -Hp
  • 观测点:此时显示的是该 Java 进程内的所有线程。找到第一行 %CPU 最高的线程,记下线程号 (例如 11146)。

步骤三:十进制 TID 转十六进制

将拿到的线程号转为小写的十六进制,用于后续匹配:

# 将十进制的线程号转换为十六进制
printf "%x\n"
  • 执行示例:printf "%x\n" 11146 输出结果为 2b8a。

步骤四:抓取堆栈,定位代码行

使用 JVM 工具抓取现场,并匹配十六进制线程号:

# 抓取整个进程的堆栈,并过滤出目标线程及其后续 20 行上下文
jstack  > /tmp/thread_dump.txt
grep -A 20 -i "0x" /tmp/thread_dump.txt
  • 分析要点:
  • 查看线程状态是否为 RUNNABLE(表明该线程正在消耗 CPU)。
  • 观察方法调用栈(at com.xxxx...),寻找业务代码包名。
  • 定位到具体行号(如 at com.company.OrderService.process(OrderService.java:11)),交由研发确认是否存在死循环或复杂计算。

03 真实生产故障现象与案例库

案例 1:业务逻辑死循环 (While(true))

  • 监控现象:CPU 呈直线上升并持续处于多核 100%,系统接口阻塞,无磁盘 IO 突增。
  • 排查结果:通过 top -Hp 和 jstack 抓取后,发现最高 CPU 线程状态为 RUNNABLE,堆栈停留在某段处理订单逻辑的 while 循环中。
  • 根因与处置:代码状态轮询逻辑退出条件异常,导致死循环。处置方案为隔离异常节点,研发修改退出条件后重新发布。

案例 2:正则引发的灾难性回溯 (Catastrophic Backtracking)

  • 监控现象:调用特定参数的接口时,单核 CPU 瞬间达到 100%。
  • 排查结果:jstack 堆栈显示高耗能线程停留在 java.util.regex.Matcher.find()。
  • 根因与处置:代码使用了复杂的正则表达式(如嵌套带有 * 或 + 的分组)校验超长文本,触发大量回溯计算。处置方案为优化正则表达式或改用标准字符串 API。

案例 3:频繁 Full GC 导致的 CPU 飙升

  • 监控现象:CPU 达到 100%,但接口并无大计算量业务。
  • 排查结果:执行 top -Hp 发现多个线程并列占用高 CPU。转换十六进制后,jstack 显示这些线程名为 GC task thread。
  • 根因与处置:属于内存耗尽(OOM 边缘)导致 JVM 频繁拉起多线程进行垃圾回收。处置动作应立即转为内存排查,使用 jmap 抓取堆快照。

04 生产排障速查表(Java CPU 打满)

遇到 CPU 打满请按序单向排查:

顺序排查层级执行命令 / 观测点异常定性处置动作01定位进程top (按 P 排序)Java 进程 CPU 极高记下 Java 进程的 <pid>02定位线程top -Hp <pid>内部某个或多个线程负载高记下占用最高的线程 <tid>03进制转换printf "%x\n" <tid>算出 16 进制值(如 2b8a)记录十六进制关键字04抓取现场jstack <pid> > dump.txt导出当前 JVM 线程快照保存现场日志05匹配代码搜索堆栈里的 0x2b8a状态 RUNNABLE 且指向业务包锁定代码行,通知研发06综合定性发现均为 GC task thread内存耗尽引发频繁 GC转入 OOM 排查流程

05 面试应答参考话术

面试官问:“线上监控报警,Java 服务 CPU 飙到 100%,接口阻塞,你怎么快速定位问题代码?”

可以这样回答:

遇到 CPU 瞬间达到 100% 的情况,不能直接重启(若生产环境需优先恢复业务,可先摘除流量或隔离单台节点后再操作)。直接重启会导致现场丢失。标准的做法是顺着“进程 → 线程 → 堆栈”链路排查,通常能快速定位到代码行。

第一步:定位异常线程

先使用 top 命令确认是 Java 进程占用较高 CPU,记下 PID。然后通过 top -Hp <pid> 查看该进程内部的所有线程,找出 %CPU 最高的线程,记下十进制线程号。

第二步:进制转换与提取堆栈

因为 top 中的线程号是十进制,而 jstack 快照中使用的是十六进制。需先用 printf "%x\n" 将线程号转换为十六进制,随后对 Java 进程执行 jstack 导出线程堆栈。

第三步:锁定代码行与归因

在堆栈文件中搜索转换后的十六进制 ID,检查该线程状态是否为 RUNNABLE。顺着方法调用栈向下看,就能找到对应的业务类和具体代码行号。如果堆栈指向业务代码,交由研发排查死循环等逻辑;如果指向的是 GC task thread,说明是内存耗尽触发了频繁 Full GC,此时会立即切换到使用 jmap 进行内存排查。

06 面试高频追问与应答预案

追问 1:如果容器没有安装 top、jstack,或者直接被资源限制杀死了怎么办?

  • 回答要点:
  • 容器内缺少工具:目前较主流的做法是使用开源诊断工具 Arthas。执行 thread -n 3 即可直接打印当前占用 CPU 最高的 3 个线程及堆栈,无需手动进制转换。
  • 容器直接重启:如果 CPU 飙升导致节点无响应并被健康检查机制重启,需要在监控系统(如 Prometheus)配置更早期的预警阈值(如 CPU > 85% 告警),在服务被强制重启前介入排查。

追问 2:如果 jstack 抓出来,发现大量线程处于 BLOCKED 状态,且 CPU 也很高,是什么原因?

  • 回答要点:
  • 大量 BLOCKED 通常意味着锁竞争严重(如死锁或重量级锁未释放)。
  • 此时 CPU 居高不下,通常是因为操作系统在频繁进行线程上下文切换(Context Switch)。排查方向应在 jstack 堆栈中寻找持有该锁的目标线程。

07 自动定位高 CPU 线程脚本(check_cpu_100.sh)

该脚本整合了上述流程,可自动提取最高 CPU 线程并打印对应的堆栈上下文。

#!/usr/bin/env bash
# ==============================================================================
# 脚本名称: check_cpu_100.sh
# 功能描述: 自动完成 PID 提取、进制转换与 jstack 堆栈截取
# 使用方式: 在运行 Java 的宿主机/容器内以 root 权限执行
# ==============================================================================

set -o pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
PLAIN='\033[0m'

echo -e "${BLUE}================================================================${PLAIN}"
echo -e "${BLUE}              Java 高 CPU 线程自动定位脚本                      ${PLAIN}"
echo -e "${BLUE}================================================================${PLAIN}"

# 1. 查找占用 CPU 最高的 Java 进程
JAVA_PID=$(top -b -n 1 -c | grep -i java | grep -v grep | head -n 1 | awk '{print $1}')

if [ -z "$JAVA_PID" ]; then
    echo -e "${RED}[ERROR] 当前系统未探测到正在运行的 Java 进程。${PLAIN}"
    exit 1
fi

echo -e "${GREEN}[INFO] 锁定最高消耗的 Java 进程 PID: ${JAVA_PID}${PLAIN}"

# 2. 检查 jstack 工具
if ! command -v jstack &>/dev/null; then
    echo -e "${RED}[ERROR] 当前环境缺少 jstack 工具,无法抓取堆栈!${PLAIN}"
    exit 1
fi

# 3. 找出进程内占用 CPU 最高的线程
TOP_THREAD_LINE=$(top -b -n 1 -H -p "$JAVA_PID" | grep -v grep | grep -E "^ *[0-9]+" | head -n 1)
TOP_TID=$(echo "$TOP_THREAD_LINE" | awk '{print $1}')
TOP_CPU=$(echo "$TOP_THREAD_LINE" | awk '{print $9}')

if [ -z "$TOP_TID" ]; then
    echo -e "${RED}[ERROR] 无法获取进程 ${JAVA_PID} 的线程信息。${PLAIN}"
    exit 1
fi

echo -e "${GREEN}[INFO] 锁定占用 CPU 最高的线程 TID: ${TOP_TID} (占比: ${TOP_CPU}\%)${PLAIN}"

# 4. 进制转换
HEX_TID=$(printf "\%x" "$TOP_TID")
echo -e "${GREEN}[INFO] 线程号十进制转十六进制: 0x${HEX_TID}${PLAIN}"

# 5. 抓取堆栈现场
echo -e "\n${YELLOW}[3/3] 正在生成 jstack 快照并截取代码上下文...${PLAIN}"
DUMP_FILE="/tmp/java_${JAVA_PID}_jstack.log"
jstack "$JAVA_PID" > "$DUMP_FILE"

echo -e "${BLUE}==================== 目标线程堆栈 ==============================${PLAIN}"
# 查找十六进制 TID,并向后打印 20 行代码栈
grep -A 20 -i "0x${HEX_TID}" "$DUMP_FILE"
echo -e "${BLUE}================================================================${PLAIN}"

echo -e "\n${YELLOW}[提示] 完整堆栈已保存至 ${DUMP_FILE},如为业务代码请交由研发排查;如为 GC thread,请排查内存配置。${PLAIN}"