ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

【JVM原理详解】56-CPU飙升排查实战

2026/8/15 18:18:23 拓冰建站 浏览量
【JVM原理详解】56-CPU飙升排查实战 CPU 飙升排查实战引言线上告警突然响起——CPU 使用率飙到 200%接口超时服务处于濒死状态。这是运维和开发最怕遇到的场景之一。CPU 飙升的原因五花八门死循环、频繁 GC、正则回溯、序列化/反序列化、加密计算……如果排查方法不对往往东试一下西试一下浪费时间。本篇将给出业界通用的排查四步法用最基础的 Linux 命令快速定位到具体线程和代码行再介绍 Arthas 的便捷替代方案和 async-profiler 火焰图分析最后通过一个完整的正则回溯案例演示端到端排查过程。排查四步法这是最经典、最通用的 CPU 飙升排查方法只需要top、printf、jstack三个工具适用于所有 Linux 环境。第一步top 找到 CPU 高的 Java 进程$top# 按 P 键按 CPU 排序或使用以下命令直接输出$top-b-n1|head-20PIDUSERPR NI VIRT RES SHR S %CPU %MEM COMMAND12345app2004.2g1.8g 12m S185.311.2java6789app2001.5g 800m 8m S2.15.0nginx...找到 CPU 最高的 Java 进程 PID如 12345。如果机器上有多个 Java 进程注意确认是哪个应用。第二步top -Hp 找到 CPU 高的线程-H参数显示进程内各线程的 CPU 占用-p指定进程 PID。$top-Hp12345PIDUSERPR NI VIRT RES SHR S %CPU COMMAND12378app2004.2g1.8g 12m R98.5java← CPU 最高的线程12379app2004.2g1.8g 12m R85.2java12380app2004.2g1.8g 12m S1.3java...记下 CPU 最高的线程 TID如 12378。注意这里的 TID 是十进制的。第三步printf 转十六进制jstack输出的线程 ID 是十六进制nid需要转换。$printf%x\n12378305a线程 TID 12378 的十六进制表示为0x305a。第四步jstack 找线程栈# 导出线程栈可选适合离线分析$ jstack12345thread_dump.txt# 或直接 grep 目标线程$ jstack12345|grep-A30nid0x305ahttp-nio-8080-exec-3#18 daemon prio5 os_prio0 tid0x7f8a01c3a000 nid0x305a runnable [0x7f8a02b1c000]java.lang.Thread.State: RUNNABLE at com.app.service.RegexValidator.validate(RegexValidator.java:42)at com.app.controller.OrderController.createOrder(OrderController.java:85)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)...找到nid0x305a的线程查看其栈顶正在执行的代码——RegexValidator.java:42。这就是 CPU 飙升的元凶所在。排查四步法全流程 top → 找到 CPU 高的进程 PID (12345) top -Hp → 找到 CPU 高的线程 TID (12378) printf %x → 十进制转十六进制 (305a) jstack → grep nid0x305a → 定位到代码行常见 CPU 飙升原因定位到代码行后需要判断具体原因。以下是五类最常见的原因和特征死循环 / 无限重试// 反例条件错误导致死循环while(flag){// flag 永远为 truedoSomething();}// 反例重试无退出条件while(true){try{callRemoteService();break;}catch(Exceptione){// 无限重试没有重试次数限制}}jstack 特征多次 dump 栈顶停留在同一行代码线程状态始终为RUNNABLE。频繁 GCGC 线程持续工作表现为 CPU 高但应用吞吐低。# 观察 GC 频率$ jstat-gcutil12345100010S0 S1 E O M FGC FGCT0.0045.2362.1098.5092.154512.341← FGC 每秒增长0.0045.2378.4598.7292.154712.8920.0045.2391.2099.0192.154913.456jstack 特征大量GC task thread处于 RUNNABLE 状态业务线程频繁处于BLOCKED或WAITING。区分方法top -Hp看 CPU 高的线程是 GC 线程名字含GC Thread、G1 Main Marker等还是业务线程。正则回溯特定输入触发正则表达式灾难性回溯Catastrophic BacktrackingCPU 瞬间打满。// 危险正则(.)* 对长字符串产生指数级回溯PatternpatternPattern.compile(^(a)$);// 输入 aaaaaaaaaaaaaaaaaaab很多 a 后面一个 b会导致 CPU 飙到 100%booleanmatchpattern.matcher(input).matches();jstack 特征栈顶停留在java.util.regex.Pattern相关方法。序列化 / 反序列化大对象的 JSON 序列化、XML 解析等消耗 CPU。// 反例超大对象 JSON 序列化StringjsonobjectMapper.writeValueAsString(hugeObjectTree);// 数十万节点jstack 特征栈顶停留在com.fasterxml.jackson、com.google.gson或java.io相关方法。加密 / 压缩计算// AES 加密、GZIP 压缩等都是 CPU 密集型byte[]compressedgzipCompress(largeData);// 大数据压缩byte[]encryptedaesEncrypt(data);// 加密jstack 特征栈顶停留在javax.crypto、java.util.zip或sun.security相关方法。Arthas 替代方案四步法虽然经典但操作步骤多且jstack在高负载时可能响应慢。Arthas的thread命令提供了一键式替代方案。thread -n 5一键找 CPU Top 5 线程# 启动 Arthas$java-jararthas-boot.jar12345[arthas12345]$ thread-n5# 展示 CPU 占用最高的 5 个线程及其栈输出直接包含线程名、CPU 占比和完整堆栈省去了 top 和 jstack 的来回切换threads5 ID NAME GROUP PRIORITY STATE %CPU TIME 42 http-nio-8080-exec-3 main 5 RUNNABLE 98.5 120.3s 43 http-nio-8080-exec-5 main 5 RUNNABLE 85.2 98.7s #42 线程栈: com.app.service.RegexValidator.validate(RegexValidator.java:42) com.app.controller.OrderController.createOrder(OrderController.java:85) ...thread 命令其他用法# 查看所有死锁[arthas12345]$ thread-b# 查看指定线程的栈[arthas12345]$ thread42# 查看处于 BLOCKED 状态的线程[arthas12345]$ thread--stateBLOCKEDthread -b能直接找到阻塞其他线程的罪魁祸首在排查锁竞争导致的 CPU 飙升时非常有效。火焰图分析当 CPU 飙升不是单个线程导致而是大量线程分散在多个方法上时四步法逐一排查效率低。火焰图Flame Graph能将所有线程的 CPU 采样可视化直观展示热点方法。async-profiler 生成火焰图async-profiler 是基于perf_events和AsyncGetCallTrace的低开销采样分析器开销通常 1%适合生产环境。# 下载并解压$tar-xzfasync-profiler-3.0-linux-x64.tar.gz# 采样 60 秒生成火焰图$ ./profiler.sh-d60-fflame.html12345# 或指定采样事件CPU、内存分配、锁$ ./profiler.sh-d60-ecpu-fcpu_flame.html12345$ ./profiler.sh-d60-ealloc-falloc_flame.html12345$ ./profiler.sh-d60-elock-flock_flame.html12345如何看火焰图用浏览器打开flame.html看到一个从下到上的调用栈堆叠图火焰图结构示意 ┌─────────────────────────────────────────────────────────┐ │ ┌──────────┐ │ │ │ matches()│ ← 栈顶方法最宽 CPU 最高 │ ┌───┴──────────┴───┐ │ │ │ Pattern.matcher() │ │ │ ┌────┴───────────────────┴────┐ │ │ │ RegexValidator.validate() │ │ │ ┌────┴──────────────────────────────┴───┐ │ │ │ OrderController.createOrder() │ │ │ ┌──┴────────────────────────────────────────┴──┐ │ │ │ HTTP Thread (base) │ │ ← 线程入口 └────────────────────────────────────────────────┘──────────┘ 横向宽度 CPU 采样占比越宽 消耗 CPU 越多 纵向高度 调用栈深度核心原则找最宽的柱子。上图中matches()方法最宽说明 CPU 主要消耗在正则匹配上。交互点击任意方框可以放大查看该方法的子调用方便逐层下钻。完整案例演示正则回溯导致 CPU 飙升现象某电商系统下单接口突然超时监控显示单个节点 CPU 飙到 198%2 核其他节点正常。触发告警时该节点 QPS 仅 50平时 2000。四步法定位# 第一步找到 Java 进程$top-b-n1|head-5PIDUSER... %CPU COMMAND12345app...198.3java# 第二步找到 CPU 高的线程$top-Hp12345|head-10PIDUSER... %CPU COMMAND12378app...98.7java12379app...95.1java# 第三步转十六进制$printf%x\n12378305a $printf%x\n12379305b# 第四步jstack 查看线程栈$ jstack12345|grep-A20nid0x305ahttp-nio-8080-exec-3#18 daemon prio5 nid0x305a runnable [0x7f8a02b1c000]java.lang.Thread.State: RUNNABLE at java.util.regex.Pattern$Curly.match0(Pattern.java:4274)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4660)at java.util.regex.Pattern$Loop.match(Pattern.java:4791)at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4660)...大量 Pattern 内部调用 at java.util.regex.Matcher.matches(Matcher.java:654)at com.app.validation.InputValidator.checkEmail(InputValidator.java:28)← 业务代码 at com.app.controller.OrderController.createOrder(OrderController.java:62)...栈顶全在java.util.regex.Pattern内部业务代码定位到InputValidator.checkEmail第 28 行。查看问题代码// InputValidator.java:28publicclassInputValidator{// 危险正则嵌套量词 (a) 导致灾难性回溯privatestaticfinalPatternEMAIL_PATTERNPattern.compile(^([a-zA-Z0-9_\\-\\.])([a-zA-Z0-9_\\-\\.])$);// ^^^^ ^^^^// 嵌套量词 和 正则引擎对特定输入产生指数级回溯publicstaticbooleancheckEmail(Stringemail){returnEMAIL_PATTERN.matcher(email).matches();// 第 28 行}}正则([a-zA-Z0-9_\-\.])中存在嵌套量词外层和内层组合对于不匹配的输入如超长字符串后跟非法字符正则引擎会尝试所有可能的分组方式产生指数级回溯。攻击者构造的恶意请求aaaa...aaaa(1000 个 a) !非法字符正则匹配尝试的路径数接近 2^1000CPU 直接打满。验证# Arthas watch 确认入参[arthas12345]$watchcom.app.validation.InputValidator checkEmail\{params, returnObj}-x2# 输出显示入参为一个超长字符串修复publicclassInputValidator{// 修复去除嵌套量词使用标准邮箱正则privatestaticfinalPatternEMAIL_PATTERNPattern.compile(^[a-zA-Z0-9_.\\-][a-zA-Z0-9_.\\-]$);// ^^^ ^^^// 单层量词无回溯爆炸风险// 或更安全直接限制输入长度publicstaticbooleancheckEmail(Stringemail){if(emailnull||email.length()254){// RFC 5321 邮箱最大 254 字符returnfalse;}returnEMAIL_PATTERN.matcher(email).matches();}}修复后 CPU 恢复正常下单接口响应时间从超时回到 50ms 以内。实践要点排查方法论先区分 GC 线程还是业务线程top -Hp后看线程名。GC 线程高说明是频繁 GC 导致应排查内存问题参见本模块第 55 篇业务线程高才走四步法定位代码。多次 jstack 对比单次 jstack 可能采样到偶然状态。连续 dump 3 次间隔 1-2 秒如果栈顶始终在同一方法基本可以确认问题点。火焰图适合复杂场景单个线程 CPU 高用四步法足够多线程分散消耗 CPU 时用 async-profiler 火焰图更高效。预防措施正则白名单对用户输入使用预编译的正则做校验禁止在运行时拼接正则。正则上线前用 Regex101 测试回溯风险。超时控制对远程调用、正则匹配等操作设置超时避免无限循环拖垮线程。限流熔断异常请求触发 CPU 告警时通过 Sentinel 等熔断器自动降级保护整体系统。CPU 告警设置 CPU 80% 持续 1 分钟告警并自动触发jstack和profiler采样保留现场。常见误区“CPU 高就是死循环”死循环只是原因之一频繁 GC、正则回溯、加密计算都会导致 CPU 高需通过 jstack 区分。“加机器就能解决”如果是正则回溯或死循环加机器只是延缓问题会随流量增长再次出现。“jstack 会影响线上”jstack 开销极低毫秒级 STW生产环境可以安全使用。但高频 jstack如每秒一次会产生大量 IO需控制频率。小结排查四步法top找进程 →top -Hp找线程 →printf %x转十六进制 →jstack | grep nid定位代码行四个命令即可从进程到代码。五大常见原因死循环、频繁 GC、正则回溯、序列化、加密计算——通过 jstack 栈顶方法区分。Arthas thread -n 5一键查看 CPU Top N 线程及其栈thread -b找阻塞源头是四步法的便捷替代。火焰图async-profiler 生成的火焰图适合多线程分散消耗 CPU 的复杂场景横向宽度即 CPU 占比找最宽的柱子。正则回溯嵌套量词如(a)对特定输入产生指数级回溯是线上 CPU 飙升的高危场景需通过简化正则 输入长度限制预防。