资讯动态

Java CPU 飙升排查实战:4 大经典场景 + 完整命令手册(2026 实战版)

发布时间:2026/10/14 17:59:35 来源:尧图企业网站定制
监控告警响起CPU 飙到 90%接口超时研发群里一片告急。别慌本文给你一套5 分钟定位 10 分钟止血 30 分钟根治的 CPU 排查 SOP配合 4 个真实生产案例覆盖死循环、上下文切换、正则回溯、序列化四大最常见 CPU 飙升场景。一、CPU 飙升通用 SOP建议先收藏无论哪种 CPU 飙升场景排查都遵循定位进程 → 定位线程 → 分析线程栈 → 根因分类 → 止血修复。1. 排查全景图告警: CPU 80% │ ▼ ┌─────────────────────┐ │ Step1: 定位进程 │ top / pidstat → 找出 Java 进程 PID └────────┬────────────┘ ▼ ┌─────────────────────┐ │ Step2: 定位线程 │ top -Hp PID → 找出 CPU 最高的线程 TID └────────┬────────────┘ ▼ ┌─────────────────────┐ │ Step3: 线程栈分析 │ jstack / arthas → TID 转 16 进制搜 nid └────────┬────────────┘ ▼ ┌─────────────────────┐ │ Step4: 根因分类 │ 死循环?锁竞争?正则?序列化? └────────┬────────────┘ ▼ ┌─────────────────────┐ │ Step5: 止血 修复 │ 扩容/限流/改代码 └─────────────────────┘2. 速查命令表# Step 1: 定位进程 top -c # 按 CPU 排序所有进程 ps -eo pid,pcpu,pmem,cmd --sort -pcpu | head 10 # Step 2: 定位线程 top -Hp PID # 找出进程内 CPU 最高的线程 ps -mp PID -o THREAD,tid,pcpu # 更详细的线程 CPU # Step 3: 线程栈分析 jstack PID /tmp/ts.log printf %x\n TID # TID 转 16 进制 # 在 ts.log 里搜: grep -A 30 nid16进制 /tmp/ts.log # Step 4: 系统层验证 uptime # 系统 loadavg vmstat 1 5 # 上下文切换、用户态/内核态 CPU cat /proc/PID/status | grep VmRSS cat /proc/PID/task | wc -l # 进程内线程数 # Step 5: arthas 一把梭 curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar dashboard # 实时面板 thread -n 3 # CPU Top 3 线程栈 thread --state BLOCKED # 所有阻塞线程 profiler start --cpu 30s # 采样 30 秒 CPU 火焰图3. 一键排查脚本cpu_check.sh#!/bin/bash # cpu_check.sh - Java 服务 CPU 飙升一键排查 PID$1 [ -z $PID ] { echo Usage: $0 PID; exit 1; } echo Step 1: 进程 CPU 概况 top -bn1 -p $PID | tail -1 echo Step 2: CPU Top 10 线程 top -bn1 -Hp $PID | head -17 echo Step 3: 线程数 上下文切换 echo 线程总数: $(ls /proc/$PID/task | wc -l) vmstat 1 3 echo Step 4: 导出 jstack jstack $PID /tmp/jstack_$(date %Y%m%d_%H%M%S).log echo 已导出: /tmp/jstack_*.log echo Step 5: GC 情况 jstat -gcutil $PID 1000 5 echo Step 6: 火焰图采样arthas echo 进入 arthas 后执行: profiler start --cpu 30s4. CPU 飙升常见模式速查┌─────────────────────────────────────────────────────────────┐ │ CPU 飙升模式识别 │ ├──────────────────┬──────────────────────────────────────────┤ │ 单线程 100% │ → 死循环 / 无限递归 / 自旋等待 │ │ 多线程均匀消耗 │ → 频繁 GC / 计算密集型任务 │ │ CPU 不高但接口慢 │ → 锁竞争 / IO 等待 / 线程池耗尽 │ │ loadavg 高 cs 大│ → 上下文切换频繁线程数过多 │ │ CPU 高 GC 高 │ → 内存泄漏/数据倾斜 │ └──────────────────┴──────────────────────────────────────────┘二、案例 1死循环 / 无限递归 —— 单线程 CPU 100%2.1 案例背景某电商订单服务在大促开始 5 分钟后单个 Java 进程 CPU 飙到 100%所有订单查询接口超时。2.2 Bug 代码// OrderService.java public ListOrder queryOrders(Long userId) { ListOrder result new ArrayList(); return findChildren(userId, userId); // ❌ 缺少终止条件 } private ListOrder findChildren(Long parentId, Long currentId) { ListOrder orders orderMapper.findByParentId(parentId); for (Order order : orders) { // ❌ 递归但没终止条件 → 无限递归 orders.addAll(findChildren(order.getId(), currentId)); } return orders; }2.3 排查过程Step 1 - top 定位进程$ top -c PID USER PR NI VIRT RES SHR S %CPU %MEM COMMAND 23841 app 20 0 8.2g 4.1g 12m R 99.8 25.3 java -jar order-service.jarStep 2 - top -Hp 定位线程$ top -Hp 23841 PID USER PR NI VIRT RES SHR S %CPU TIME COMMAND 24112 app 20 0 8.2g 4.1g 12m R 98.5 15:32.11 java ← 单线程 98.5%关键信号单线程 CPU 占满→ 大概率是死循环/无限递归。Step 3 - TID 转 16 进制$ printf %x\n 24112 5e30Step 4 - jstack 找线程$ jstack 23841 | grep -A 30 nid0x5e30 http-nio-8080-exec-42 #24112 daemon prio5 tid0x00007f1234 nid0x5e30 runnable java.lang.Thread.State: RUNNABLE at com.acme.service.OrderService.findChildren(OrderService.java:42) at com.acme.service.OrderService.findChildren(OrderService.java:48) at com.acme.service.OrderService.findChildren(OrderService.java:48) at com.acme.service.OrderService.findChildren(OrderService.java:48) ... (重复 5000 次)一眼看出栈深度爆了全是同一个方法递归。Step 5 - arthas 验证$ thread -n 1 http-nio-8080-exec-42 Id24112 cpuUsage98.5% deltaTime23456ms at com.acme.service.OrderService.findChildren(OrderService.java:42) at com.acme.service.OrderService.findChildren(OrderService.java:48) ... 栈深度 50002.4 排查思路单线程 CPU 100% ↓ TID 转 16 进制搜 jstack ↓ 栈深度异常 / 重复同一个方法 → 死循环 / 无限递归 ↓ 检查代码: 递归终止条件、循环边界2.5 应急止血# 方案 1: 临时重启最快 kill -9 23841 systemctl restart order-service # 方案 2: arthas 阻断不重启 # 进入 arthas 后用 trace 看耗时 trace com.acme.service.OrderService findChildren #cost 200 -n 5 # 方案 3: 限流牺牲部分可用性 # 用 sentinel/resilience4j 限流 orderQuery 接口2.6 修复方案// ✅ 修复 1: 增加递归深度限制 private static final int MAX_DEPTH 10; public ListOrder queryOrders(Long userId) { return findChildren(userId, userId, 0); } private ListOrder findChildren(Long parentId, Long rootId, int depth) { if (depth MAX_DEPTH) { log.warn(递归查询超过最大层级: parentId{}, rootId{}, parentId, rootId); return Collections.emptyList(); } ListOrder orders orderMapper.findByParentId(parentId); for (Order order : orders) { // ✅ 终止条件碰到根节点就停止 if (order.getId().equals(rootId)) continue; orders.addAll(findChildren(order.getId(), rootId, depth 1)); } return orders; } // ✅ 修复 2: 用迭代代替递归更安全 public ListOrder queryOrdersIterative(Long userId) { ListOrder result new ArrayList(); DequeLong stack new ArrayDeque(); SetLong visited new HashSet(); stack.push(userId); while (!stack.isEmpty() result.size() 10000) { // ✅ 显式上限 Long parentId stack.pop(); if (visited.contains(parentId)) continue; visited.add(parentId); ListOrder orders orderMapper.findByParentId(parentId); for (Order o : orders) stack.push(o.getId()); result.addAll(orders); } return result; }三、案例 2频繁上下文切换 —— loadavg 暴涨但 CPU 不一定满2.1 案例背景某报表服务导出 Excel 时CPU 没到 100%约 60%但系统loadavg 高达 328 核机器vmstat cs上下文切换 10 万/秒所有接口响应变慢。2.2 Bug 代码// ReportService.java public void exportReport(Long reportId) { ListDataRow allData reportMapper.findAllData(reportId); // ❌ 一个请求 new 1000 个 Thread 处理数据分片 ListThread threads new ArrayList(); for (DataChunk chunk : chunks(allData, 100)) { Thread t new Thread(() - { synchronized (configLock) { // ❌ 全局锁 enrichData(chunk); } saveChunk(chunk); }); threads.add(t); t.start(); } for (Thread t : threads) { try { t.join(); } catch (InterruptedException e) { ... } } }2.3 排查过程Step 1 - 看系统负载$ uptime 14:23:18 up 30 days, load average: 32.15, 28.73, 25.41 ← 异常高 $ nproc 8 ← 只有 8 核loadavg 32 CPU 核数 8 × 4说明有大量进程/线程在排队。Step 2 - vmstat 看上下文切换$ vmstat 1 5 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 48 2 0 512M 128M 2.1G 0 0 12 156 12450 89523 78 22 0 0 0 52 3 0 510M 128M 2.1G 0 0 15 148 13520 102340 76 24 0 0 0关键信号-r运行队列48-52→ 远高于 CPU 核数 8-cs上下文切换89523-102340/秒→ 上下文切换太频繁-us用户态76-78%→ CPU 大量时间被用掉Step 3 - 进程线程数$ ls /proc/15891/task | wc -l 8230 ← 进程内 8230 个线程正常应用 500Step 4 - jstack 看线程状态分布$ jstack 15891 | grep java.lang.Thread.State | sort | uniq -c 5234 java.lang.Thread.State: RUNNABLE # 5000 线程可运行 3000 java.lang.Thread.State: BLOCKED # 3000 线程阻塞Step 5 - arthas 找锁竞争元凶$ thread -b # 找出阻塞其他线程的元凶 pool-3-thread-15 at com.acme.service.ReportService.enrichData(ReportService.java:88) - waiting to lock 0x000000076b2c8e90 (a java.lang.Object) BLOCKED 5800 threads ← 一个线程持锁5800 个线程在等2.4 排查思路CPU 不满但接口慢 ↓ 看 loadavg vmstat cs ↓ loadavg 高 cs 暴涨 → 线程数过多 上下文切换频繁 ↓ 看 jstack 状态分布: BLOCKED 扎堆 → 锁竞争 ↓ arthas thread -b → 找到持锁元凶2.5 应急止血# 方案 1: 暂停大查询任务释放线程池 # 找到调用方导出报表接口临时降级或限流 # 方案 2: 限流 排队 # 用 sentinel 给 exportReport 限流避免同时发起太多 # 方案 3: 紧急调小线程栈重启 java -Xss256k -jar report-service.jar # 线程栈从 1MB 降到 256KB2.6 修复方案// ✅ 修复 1: 用线程池替代 new Thread() Service public class ReportService { Autowired private ThreadPoolExecutor reportExecutor; // 见下方配置 public void exportReport(Long reportId) { ListDataRow allData reportMapper.findAllData(reportId); ListDataChunk chunks chunks(allData, 100); // ✅ 提交到线程池线程数可控 ListCompletableFutureVoid futures chunks.stream() .map(chunk - CompletableFuture.runAsync( () - processChunk(chunk), reportExecutor)) .collect(Collectors.toList()); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } } // ✅ 修复 2: 锁粒度细化按 chunkId 加锁 private final ConcurrentHashMapLong, Object chunkLocks new ConcurrentHashMap(); public void enrichData(DataChunk chunk) { Object lock chunkLocks.computeIfAbsent(chunk.getId(), k - new Object()); synchronized (lock) { // ✅ 锁粒度细化 // 业务逻辑 } } // ✅ 修复 3: 读写锁替代 synchronized private final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); public void readConfig() { rwLock.readLock().lock(); try { /* 读 */ } finally { rwLock.readLock().unlock(); } } public void updateConfig() { rwLock.writeLock().lock(); try { /* 写 */ } finally { rwLock.writeLock().unlock(); } } // ✅ 修复 4: 线程池合理配置 Bean public ThreadPoolExecutor reportExecutor() { return new ThreadPoolExecutor( 10, // 核心线程 50, // 最大线程 60, TimeUnit.SECONDS, new ArrayBlockingQueue(2000), // 有界队列 new ThreadPoolExecutor.CallerRunsPolicy() ); } // JVM 参数 // -Xss512k # 线程栈调小单进程支持更多线程四、案例 3正则表达式灾难性回溯 —— CPU 跑满且 GC 异常3.1 案例背景某风控服务在压测时CPU 持续 95%接口全部超时但内存和线程数都没异常。3.2 Bug 代码// RiskControlService.java public boolean checkRisk(String input) { // ❌ 经典灾难性回溯正则 // 模式: 校验包含字母数字组合的字符串 String pattern ^(\\w)*[A-Z]$; return input.matches(pattern); } // ❌ 更隐蔽的写法用 Pattern.compile 编译后复用 private static final Pattern RISK_PATTERN Pattern.compile(^(\\w)*[A-Z]$); public boolean checkRiskV2(String input) { Matcher matcher RISK_PATTERN.matcher(input); return matcher.matches(); }3.3 排查过程Step 1 - top 定位$ top -c PID USER PR NI VIRT RES SHR S %CPU %MEM COMMAND 17891 app 20 0 8.2g 3.8g 12m R 96.5 23.2 java -jar risk-service.jarStep 2 - top -Hp 定位线程$ top -Hp 17891 PID USER PR NI VIRT RES SHR S %CPU TIME COMMAND 18001 app 20 0 8.2g 3.8g 12m R 25.3 5:32.11 java 18002 app 20 0 8.2g 3.8g 12m R 24.8 5:30.89 java 18003 app 20 0 8.2g 3.8g 12m R 24.5 5:31.45 java ... (12 个 http 线程均匀消耗 CPU每个 24%)关键信号多线程均匀消耗 CPU不是单线程死循环。Step 3 - arthas 看线程栈$ thread -n 3 http-nio-8080-exec-23 Id18001 cpuUsage25.3% deltaTime312ms at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658) at java.util.regex.Pattern$Loop.match(Pattern.java:5123) at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658) ... (regex 循环) http-nio-8080-exec-24 Id18002 cpuUsage24.8% at java.util.regex.Pattern$GroupHead.match(Pattern.java:4658) ...关键信号栈里全是java.util.regex.Pattern.*match***→ 正则匹配在死循环Step 4 - 看 CPU 时间分布火焰图# arthas profiler $ profiler start --cpu 30s $ profiler: [xx] java.util.regex.Pattern$Loop.match 75.2% ← 正则匹配占 75% [xx] java.util.regex.Pattern$GroupHead.match 12.3% [xx] com.acme.service.RiskControlService.checkRisk 8.5%Step 5 - jstack 看 GC 情况$ jstat -gcutil 17891 1000 3 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 12.50 0.00 45.20 85.73 95.12 89.34 1250 45.23 12 156.78 201.99GC 也高YGC 每分钟数百次因为正则匹配在堆上创建大量对象。3.4 排查思路多线程均匀消耗 CPU 内存对象增长 ↓ arthas thread -n → 栈里全是 java.util.regex ↓ profiler 火焰图 → Pattern.match 占 75% ↓ 代码定位 → 灾难性回溯正则3.5 应急止血# 方案 1: 临时把风控接口熔断 SentinelResource(value checkRisk, blockHandler fallback) public boolean checkRisk(String input) { ... } # 方案 2: arthas 阻断不重启 # 进入 arthas 后 trace com.acme.service.RiskControlService checkRisk #cost 200 -n 5 # 找到问题 regex 调用 # 方案 3: 紧急替换正则 # 不用重启把正则改成不灾难性回溯的版本见下方3.6 修复方案// ✅ 修复 1: 避免嵌套量词 占有优先量词 // ❌ 灾难性回溯: ^(\\w)*[A-Z]$ // ✅ 修复后: 用占有优先 避免嵌套 private static final Pattern SAFE_PATTERN Pattern.compile(^\\w*[A-Z]\\w*$); public boolean checkRisk(String input) { return SAFE_PATTERN.matcher(input).matches(); } // ✅ 修复 2: 预校验长度最快 public boolean checkRisk(String input) { // ✅ 先判断长度短的字符串不可能触发灾难性回溯 if (input.length() 100 || input.isEmpty()) return false; String pattern ^(\\w)*[A-Z]$; return input.matches(pattern); } // ✅ 修复 3: 用 Re2JGoogle 正则库线性时间无回溯 // https://github.com/google/re2j re2.Pattern pattern re2.Pattern.compile(^(\\w)*[A-Z]$); return pattern.matcher(input).matches(); // ✅ 修复 4: 自动化检测 报警推荐 // 使用以下正则模式自动检测: // - (a) / (a*)* // - (a|a)* // - a*a* // 这些模式在代码评审时一定要拦下来五、案例 4高频序列化 / 反序列化 —— CPU 持续高位4.1 案例背景某网关服务做 API 网关加解密CPU 持续 70%-85%但没有 GC 异常、没有线程问题只有 CPU 跑满。4.2 Bug 代码// ApiGatewayService.java Service public class ApiGatewayService { private static final ObjectMapper MAPPER new ObjectMapper(); public ApiResponse handleRequest(String rawPayload) { try { // ❌ 每个请求都 new ObjectMapper ObjectMapper mapper new ObjectMapper(); // ❌ 高频反序列化 Request req mapper.readValue(rawPayload, Request.class); // 业务处理 Response resp processLogic(req); // ❌ 高频序列化 String json mapper.writeValueAsString(resp); // ❌ 每次都 Base64 编码 String encoded Base64.getEncoder().encodeToString(json.getBytes()); return new ApiResponse(encoded); } catch (Exception e) { log.error(网关处理失败, e); throw new BizException(网关异常); } } }4.3 排查过程Step 1 - top 定位$ top -c PID USER PR NI VIRT RES SHR S %CPU %MEM COMMAND 20311 app 20 0 8.2g 3.5g 12m R 78.5 22.1 java -jar gateway.jarStep 2 - top -Hp 看线程分布$ top -Hp 20311 PID USER PR NI VIRT RES SHR S %CPU TIME COMMAND 20501 app 20 0 8.2g 3.5g 12m R 12.5 3:12.11 java 20502 app 20 0 8.2g 3.5g 12m R 12.3 3:11.89 java ... (N 个 http-nio 线程均匀消耗每个 12% CPU)Step 3 - arthas profiler 火焰图$ profiler start --cpu 30s [xx] com.fasterxml.jackson.databind.ObjectMapper.readValue 45.2% ← JSON 反序列化 45% [xx] com.fasterxml.jackson.databind.ObjectMapper.writeValue 23.5% ← JSON 序列化 23.5% [xx] java.util.Base64$Encoder.encode 8.2% [xx] com.acme.gateway.ApiGatewayService.handleRequest 15.3% [xx] ... (其他)关键信号Jackson 序列化/反序列化占 68% CPU。Step 4 - arthas watch 方法$ watch com.acme.gateway.ApiGatewayService handleRequest {params, returnObj, throwExp} -x 54.4 排查思路CPU 高但内存/线程/GC 都正常 ↓ arthas profiler → 火焰图找 CPU 热点 ↓ 热点是序列化/反序列化 → 优化 JSON 处理4.5 应急止血# 方案 1: 临时限流牺牲可用性 SentinelResource(value handleRequest, blockHandler fallback) # 方案 2: 临时跳过非关键序列化 # 在 arthas 里 ognl 修改开关 ognl com.acme.gateway.ApiGatewayServiceenabledfalse # 方案 3: 扩容网关实例加机器4.6 修复方案// ✅ 修复 1: ObjectMapper 复用不要每次 new Service public class ApiGatewayService { // ✅ 单例 ObjectMapper线程安全 private static final ObjectMapper MAPPER new ObjectMapper(); public ApiResponse handleRequest(String rawPayload) { try { // ✅ 直接用单例 Request req MAPPER.readValue(rawPayload, Request.class); Response resp processLogic(req); String json MAPPER.writeValueAsString(resp); // ... } } } // ✅ 修复 2: 开启 Jackson 性能优化 ObjectMapper mapper new ObjectMapper(); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); // ✅ 关键用 AfterburnerModule基于字节码生成性能提升 30-40% mapper.registerModule(new AfterburnerModule()); // ✅ 修复 3: 复杂场景用 Fastjson2更快或Protobuf更快更省空间 // https://github.com/alibaba/fastjson2 JSONArray array JSON.parseArray(rawPayload); // ✅ 修复 4: 加本地缓存对相同 payload Cacheable(value gateway:req, key #rawPayload.hashCode()) public ApiResponse handleRequest(String rawPayload) { ... } // ✅ 修复 5: JVM 参数优化启动时定长堆减少 GC // -Xms4g -Xmx4g -XX:UseG1GC -XX:MaxGCPauseMillis200六、CPU 飙升排查 Checklist直接拿去做 Runbook排查步骤命令 / 工具关键指标 / 关注点1. 定位进程top -c/pidstat%CPU 80% 的 Java 进程2. 定位线程top -Hp PID/ps -mp单线程接近 100% 死循环多线程均匀 计算密集3. 线程栈jstack/arthas thread -nRUNNABLE 线程在执行什么方法4. 上下文切换vmstat 1/uptimecs 5w/秒 线程数过多5. 线程数ls /proc/PID/task \| wc -l线程数 1000 线程泄漏6. GC 检查jstat -gcutil/arthas dashboardFGC 频率 GC 线程占用7. 锁竞争arthas thread -b/thread --state BLOCKEDBLOCKED 扎堆等同一把锁8. 火焰图arthas profiler startCPU 时间分布找热点方法9. 业务调用arthastrace/watch观测方法入参、返回、耗时七、写在最后CPU 飙升问题没有银弹但有固定套路先定位进程 → 线程 → 代码自顶向下看模式识别单线程满 死循环多线程均匀 计算密集CPU 不满但接口慢 锁先应急止血 → 再彻底修复恢复服务优先加监控 告警CPU 70% 提前预警不要等 90% 才动手把上面 4 个案例 SOP 收藏到你的 Runbook下次遇到 CPU 飙升直接照抄作业。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑