Java應(yīng)用CPU占用過(guò)高問(wèn)題的快速定位和解決方法

問(wèn)題背景
在生產(chǎn)環(huán)境中,我們經(jīng)常會(huì)遇到Java應(yīng)用突然CPU占用飆升的情況。這種問(wèn)題如果不及時(shí)解決,可能會(huì)耗盡系統(tǒng)資源,影響整個(gè)應(yīng)用的穩(wěn)定性。本文將介紹一套快速定位和解決Java應(yīng)用CPU占用過(guò)高問(wèn)題的標(biāo)準(zhǔn)流程,并通過(guò)一個(gè)實(shí)際案例演示。
排查工具與核心思路
核心思路:進(jìn)程 → 線程 → 線程棧 → 源代碼
主要工具:
top:查看系統(tǒng)進(jìn)程資源占用情況
top -Hp:查看指定進(jìn)程下的線程資源占用
printf:將線程ID轉(zhuǎn)換為十六進(jìn)制
jstack:獲取Java進(jìn)程的線程堆棧信息
grep:過(guò)濾匹配的線程堆棧信息
詳細(xì)排查步驟
步驟1:定位CPU占用最高的進(jìn)程
使用top命令查看系統(tǒng)進(jìn)程資源占用情況:
top
重點(diǎn)關(guān)注%CPU列,找到CPU占用比例最高的進(jìn)程,并記錄其PID(進(jìn)程ID)。
步驟2:定位進(jìn)程內(nèi)CPU占用最高的線程
使用以下命令查看指定進(jìn)程內(nèi)所有線程的CPU占用情況:
top -Hp [PID]
其中[PID]是上一步記錄的進(jìn)程ID。找到CPU占用最高的線程,記錄其線程ID。
步驟3:轉(zhuǎn)換線程ID為十六進(jìn)制
Java的線程堆棧信息中的線程ID是以十六進(jìn)制表示的,需要轉(zhuǎn)換:
printf "0x%x" [線程ID]
記錄轉(zhuǎn)換后的十六進(jìn)制線程ID。
步驟4:定位問(wèn)題代碼
使用jstack獲取線程堆棧,并結(jié)合grep過(guò)濾出問(wèn)題線程的堆棧信息:
jstack [PID] | grep [十六進(jìn)制線程ID] -A 10
-A 10參數(shù)表示顯示匹配行后的10行內(nèi)容,這通常能包含足夠的方法調(diào)用信息來(lái)定位問(wèn)題代碼。
實(shí)戰(zhàn)案例演示
場(chǎng)景描述
某Java應(yīng)用在壓測(cè)期間CPU占用率突然飆升到200%以上,需要快速定位問(wèn)題原因。
排查過(guò)程
1. 定位高CPU占用進(jìn)程
top
輸出結(jié)果:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND12345 appuser 20 0 12.3g 2.1g 1.2g R 215.6 6.7 10:30.25 java
發(fā)現(xiàn)PID為12345的Java進(jìn)程CPU占用率高達(dá)215.6%。
2. 定位進(jìn)程內(nèi)高CPU占用線程
top -Hp 12345
輸出結(jié)果:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND12367 appuser 20 0 12.3g 2.1g 1.2g R 99.8 6.7 8:45.12 java12368 appuser 20 0 12.3g 2.1g 1.2g R 99.7 6.7 8:44.98 java
發(fā)現(xiàn)線程ID為12367和12368的兩個(gè)線程CPU占用率都很高。
3. 轉(zhuǎn)換線程ID為十六進(jìn)制
printf "0x%x" 12367
輸出:0x304f
printf "0x%x" 12368
輸出:0x3050定位問(wèn)題代碼
jstack 12345 | grep "0x304f" -A 10
輸出結(jié)果:
"Thread-0" #20 prio=5 os_prio=0 tid=0x00007f8a1410c800 nid=0x304f runnable [0x00007f8a0c7f7000]
java.lang.Thread.State: RUNNABLE
at com.example.ExampleService.processData(ExampleService.java:45)
at com.example.ExampleService$$Lambda$1.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)
同樣檢查另一個(gè)高CPU線程:
jstack 12345 | grep "0x3050" -A 10
輸出結(jié)果:
"Thread-1" #21 prio=5 os_prio=0 tid=0x00007f8a1410d000 nid=0x3050 runnable [0x00007f8a0c6f6000]
java.lang.Thread.State: RUNNABLE
at com.example.ExampleService.processData(ExampleService.java:45)
at com.example.ExampleService$$Lambda$1.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)
問(wèn)題分析與解決
兩個(gè)高CPU線程都指向ExampleService.java的第45行。查看源代碼:
public class ExampleService {
public void processData() {
while (true) {
// 第45行 - 空循環(huán),沒(méi)有退出條件
// 這里應(yīng)該是處理業(yè)務(wù)的邏輯,但誤寫成了死循環(huán)
}
}
}
問(wèn)題原因:代碼中誤寫了死循環(huán),導(dǎo)致線程持續(xù)占用CPU資源。
解決方案:
添加合理的循環(huán)退出條件
在循環(huán)體內(nèi)添加適當(dāng)?shù)膕leep或等待邏輯
修復(fù)后的代碼:
public class ExampleService {
public void processData() {
while (!Thread.currentThread().isInterrupted()) {
// 業(yè)務(wù)處理邏輯
processBusiness();
// 添加適當(dāng)?shù)男菝?,避免空轉(zhuǎn)
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
常見(jiàn)CPU高占用場(chǎng)景
死循環(huán):如案例所示,缺少退出條件的循環(huán)
頻繁GC:大量對(duì)象創(chuàng)建和回收
復(fù)雜算法:遞歸過(guò)深或計(jì)算復(fù)雜度高的操作
鎖競(jìng)爭(zhēng):線程頻繁嘗試獲取鎖
無(wú)限遞歸:缺少基準(zhǔn)情況的遞歸調(diào)用
預(yù)防措施
代碼審查:重點(diǎn)關(guān)注循環(huán)和遞歸邏輯
性能測(cè)試:定期進(jìn)行壓力測(cè)試,監(jiān)控CPU使用情況
監(jiān)控告警:建立完善的監(jiān)控體系,設(shè)置CPU使用率閾值告警
代碼規(guī)范:避免在循環(huán)內(nèi)進(jìn)行不必要的復(fù)雜計(jì)算
總結(jié)
通過(guò)top → top -Hp → printf → jstack + grep這一套組合拳,我們可以快速定位到導(dǎo)致CPU占用過(guò)高的具體代碼位置。掌握這套排查流程對(duì)于Java開(kāi)發(fā)者來(lái)說(shuō)至關(guān)重要,能夠在生產(chǎn)環(huán)境出現(xiàn)問(wèn)題時(shí)快速響應(yīng)和解決,保障系統(tǒng)的穩(wěn)定運(yùn)行。
以下是一個(gè)完整的定位Java進(jìn)程CPU占用過(guò)高問(wèn)題的腳本
#!/bin/bash
# CPU高占用排查腳本
# 功能:自動(dòng)定位Java進(jìn)程中CPU占用最高的線程并顯示相關(guān)堆棧信息
# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 打印顏色輸出函數(shù)
print_color() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# 檢查必要命令是否存在
check_commands() {
local commands=("top" "printf" "jstack" "grep" "awk")
for cmd in "${commands[@]}"; do
if ! command -v "$cmd" &> /dev/null; then
print_color "$RED" "錯(cuò)誤: 未找到命令 $cmd,請(qǐng)確保已安裝"
exit 1
fi
done
}
# 顯示使用說(shuō)明
show_usage() {
echo "用法: $0 [選項(xiàng)]"
echo "選項(xiàng):"
echo " -h, --help 顯示幫助信息"
echo " -l, --lines 指定顯示行數(shù) (默認(rèn): 10)"
echo " -p, --process 指定進(jìn)程名 (默認(rèn): java)"
echo ""
echo "示例:"
echo " $0 # 使用默認(rèn)設(shè)置"
echo " $0 -l 20 # 顯示20行堆棧信息"
echo " $0 -p tomcat # 指定進(jìn)程名為tomcat"
echo " $0 -p java -l 15 # 指定進(jìn)程名和顯示行數(shù)"
}
# 解析命令行參數(shù)
PROCESS_NAME="java"
LINES=10
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_usage
exit 0
;;
-l|--lines)
LINES="$2"
shift 2
;;
-p|--process)
PROCESS_NAME="$2"
shift 2
;;
*)
print_color "$RED" "未知參數(shù): $1"
show_usage
exit 1
;;
esac
done
# 主函數(shù)
main() {
print_color "$BLUE" "================================================"
print_color "$BLUE" " Java CPU高占用排查工具"
print_color "$BLUE" "================================================"
echo ""
# 檢查必要命令
check_commands
print_color "$YELLOW" "正在查找進(jìn)程名為 '$PROCESS_NAME' 的進(jìn)程..."
echo ""
# 獲取CPU占用最高的Java進(jìn)程PID
local top_pid=$(top -bn1 | grep -i "$PROCESS_NAME" | head -10 | awk 'NR>7 {if($9+0 > 0) print $1,$9}' | sort -k2 -nr | head -1 | awk '{print $1}')
if [[ -z "$top_pid" ]]; then
print_color "$RED" "未找到進(jìn)程名為 '$PROCESS_NAME' 且CPU占用大于0的進(jìn)程!"
exit 1
fi
local cpu_usage=$(top -bn1 -p "$top_pid" | grep "$top_pid" | awk '{print $9}')
print_color "$GREEN" "? 找到目標(biāo)進(jìn)程:"
echo " PID: $top_pid"
echo " 進(jìn)程名: $PROCESS_NAME"
echo " CPU使用率: ${cpu_usage}%"
echo ""
# 獲取進(jìn)程詳細(xì)信息
print_color "$YELLOW" "獲取進(jìn)程詳細(xì)信息..."
local process_info=$(ps -p "$top_pid" -o pid,ppid,user,pcpu,pmem,cmd --no-headers)
echo " 進(jìn)程信息: $process_info"
echo ""
# 獲取CPU占用最高的線程
print_color "$YELLOW" "正在分析進(jìn)程 $top_pid 中的線程..."
local top_thread=$(top -Hbn1 -p "$top_pid" | awk 'NR>7 {if($9+0 > 0) print $1,$9,$12}' | sort -k2 -nr | head -1)
if [[ -z "$top_thread" ]]; then
print_color "$RED" "未找到CPU占用大于0的線程!"
exit 1
fi
local thread_id=$(echo "$top_thread" | awk '{print $1}')
local thread_cpu=$(echo "$top_thread" | awk '{print $2}')
local thread_name=$(echo "$top_thread" | awk '{for(i=3;i<=NF;i++) printf $i" "; print ""}' | sed 's/ *$//')
print_color "$GREEN" "? 找到CPU占用最高的線程:"
echo " 線程ID: $thread_id"
echo " 線程名: $thread_name"
echo " CPU使用率: ${thread_cpu}%"
echo ""
# 轉(zhuǎn)換線程ID為十六進(jìn)制
print_color "$YELLOW" "轉(zhuǎn)換線程ID為十六進(jìn)制..."
local hex_thread_id=$(printf "0x%x" "$thread_id")
echo " 線程ID(十進(jìn)制): $thread_id"
echo " 線程ID(十六進(jìn)制): $hex_thread_id"
echo ""
# 獲取用戶輸入的顯示行數(shù)
print_color "$YELLOW" "請(qǐng)輸入要顯示的堆棧信息行數(shù) (回車使用默認(rèn)值 $LINES): "
read -r user_lines
if [[ -n "$user_lines" && "$user_lines" =~ ^[0-9]+$ ]]; then
LINES="$user_lines"
fi
echo ""
print_color "$BLUE" "正在使用 jstack 分析線程堆棧 (顯示 $LINES 行)..."
print_color "$BLUE" "================================================"
# 使用jstack獲取線程堆棧信息
local jstack_output
if jstack_output=$(jstack "$top_pid" 2>/dev/null); then
echo "$jstack_output" | grep -A "$LINES" "$hex_thread_id"
else
print_color "$RED" "執(zhí)行 jstack 失敗!可能的原因:"
echo " 1. 進(jìn)程 $top_pid 不存在"
echo " 2. 沒(méi)有執(zhí)行 jstack 的權(quán)限"
echo " 3. 進(jìn)程不是Java進(jìn)程"
echo " 4. JAVA_HOME環(huán)境變量未正確設(shè)置"
# 嘗試使用/proc文件系統(tǒng)獲取信息
print_color "$YELLOW" "嘗試通過(guò)其他方式獲取線程信息..."
echo "線程狀態(tài):"
cat "/proc/$top_pid/task/$thread_id/status" | grep -E "^(Name|State|Pid):" 2>/dev/null || echo "無(wú)法獲取線程狀態(tài)信息"
fi
echo ""
print_color "$BLUE" "================================================"
print_color "$GREEN" "? 分析完成!"
echo ""
print_color "$YELLOW" "建議后續(xù)操作:"
echo " 1. 查看堆棧信息中的代碼位置"
echo " 2. 檢查對(duì)應(yīng)的Java源代碼"
echo " 3. 分析是否存在死循環(huán)、頻繁GC等問(wèn)題"
echo " 4. 使用 profiler 工具進(jìn)行深入分析"
}
# 錯(cuò)誤處理
set -e
# 捕捉Ctrl+C
trap 'echo ""; print_color "$RED" "用戶中斷操作"; exit 1' INT
# 運(yùn)行主函數(shù)
main "$@"
保存腳本
將上面的腳本保存為 cpu_debug.sh,并給予執(zhí)行權(quán)限:
chmod +x cpu_debug.sh
運(yùn)行方式
基本用法(默認(rèn)Java進(jìn)程,顯示10行):
./cpu_debug.sh
指定顯示行數(shù):
./cpu_debug.sh -l 20
指定進(jìn)程名:
./cpu_debug.sh -p tomcat
組合使用:
./cpu_debug.sh -p java -l 15
查看幫助:
./cpu_debug.sh -h
流程圖 (Flowchart)
下圖展示了定位CPU占用過(guò)高問(wèn)題的整體步驟與決策邏輯。
flowchart TD
A[Java應(yīng)用CPU占用飆升] --> B[使用 top 命令<br>定位高CPU進(jìn)程]
B --> C{找到目標(biāo)Java進(jìn)程?}
C -- 是 --> D[使用 top -Hp [PID]<br>定位高CPU線程]
C -- 否 --> A
D --> E[使用 printf<br>轉(zhuǎn)換線程ID為十六進(jìn)制]
E --> F[使用 jstack + grep<br>定位問(wèn)題堆棧]
F --> G[分析堆棧信息<br>定位問(wèn)題代碼(如死循環(huán))]
G --> H[修復(fù)代碼并發(fā)布]

序列圖 (Sequence Diagram)
下圖詳細(xì)描述了排查過(guò)程中,用戶與各個(gè)系統(tǒng)工具之間的交互時(shí)序。
sequenceDiagram
participant U as 用戶/運(yùn)維
participant T as top 命令
participant P as 問(wèn)題Java進(jìn)程
participant S as jstack 命令
participant G as grep 命令
Note over U: 開(kāi)始排查
U ->> T: 執(zhí)行 top
T ->> U: 返回進(jìn)程列表<br>(發(fā)現(xiàn)高CPU進(jìn)程PID)
U ->> T: 執(zhí)行 top -Hp [PID]
T ->> U: 返回線程列表<br>(發(fā)現(xiàn)高CPU線程TID)
U ->> P: 執(zhí)行 printf “0x%x” [TID]
P ->> U: 返回十六進(jìn)制線程ID (nid)
U ->> S: 執(zhí)行 jstack [PID]
S ->> U: 輸出完整線程堆棧
U ->> G: 使用 grep 過(guò)濾 nid
G ->> U: 返回問(wèn)題線程的堆棧<br>(定位到具體代碼行)
Note over U: 分析代碼并修復(fù)問(wèn)題

以上就是Java應(yīng)用CPU占用過(guò)高問(wèn)題的快速定位和解決方法的詳細(xì)內(nèi)容,更多關(guān)于Java CPU占用過(guò)高的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Springboot集成Proguard生成混淆jar包方式
本文介紹了兩種Java代碼混淆工具:ClassFinal和ProGuard,ClassFinal是一個(gè)字節(jié)碼加密工具,但需要額外的加密包,使用復(fù)雜,ProGuard是一款開(kāi)源的Java代碼混淆工具,可以有效地提高代碼的安全性,但對(duì)Spring框架的注解處理不夠完善2024-11-11
基于mybatis查詢結(jié)果映射不到對(duì)象的處理
這篇文章主要介紹了mybatis查詢結(jié)果映射不到對(duì)象的處理方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
Idea安裝及涉及springboot詳細(xì)配置的圖文教程
這篇文章主要介紹了Idea安裝及涉及springboot詳細(xì)配置,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10
Java 數(shù)據(jù)類型及類型轉(zhuǎn)換的互相轉(zhuǎn)換實(shí)例代碼
這篇文章主要介紹了Java 數(shù)據(jù)類型及類型轉(zhuǎn)換的互相轉(zhuǎn)換實(shí)例代碼,需要的朋友可以參考下2020-10-10
Spring Boot使用線程池創(chuàng)建多線程的完整示例
在 Spring Boot 2 中,可以使用 @Autowired 注入 線程池(ThreadPoolTaskExecutor 或 ExecutorService),從而管理線程的創(chuàng)建和執(zhí)行,以下是使用 @Autowired 方式注入線程池的完整示例,感興趣的朋友一起看看吧2025-03-03
MyBatis實(shí)現(xiàn)注冊(cè)及獲取Mapper
本文主要介紹了MyBatis實(shí)現(xiàn)注冊(cè)及獲取Mapper,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-03-03
詳解如何為SpringBoot Web應(yīng)用的日志方便追蹤
在Web應(yīng)用程序領(lǐng)域,有效的請(qǐng)求監(jiān)控和可追溯性對(duì)于維護(hù)系統(tǒng)完整性和診斷問(wèn)題至關(guān)重要,SpringBoot是一種用于構(gòu)建Java應(yīng)用程序的流行框架,在本文中,我們探討了在SpringBoot中向日志添加唯一ID的重要性,需要的朋友可以參考下2023-11-11
Java實(shí)現(xiàn)Excel文件加密解密的示例代碼
設(shè)置excel文件保護(hù)時(shí),通??蛇x擇對(duì)整個(gè)工作簿進(jìn)行加密保護(hù)。無(wú)需設(shè)置文檔保護(hù)時(shí),可撤銷密碼保護(hù),即解密文檔。本文將通過(guò)java程序演示以上加密、解密方法的實(shí)現(xiàn),感興趣的可以了解一下2022-05-05
Java多線程連續(xù)打印abc實(shí)現(xiàn)方法詳解
這篇文章主要介紹了Java多線程連續(xù)打印abc實(shí)現(xiàn)方法詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03

