本文已收录于《JVM生产环境问题定位与解决实战》专栏,完整系列见文末目录
1. 引言
在上一篇文章中,我们深入剖析了OSSClient泄漏引发的FullGC风暴全链路排查过程。本文聚焦另一个经典线上问题——正则表达式回溯导致的CPU 100%。在Java应用中,正则表达式使用普遍,但设计缺陷可能引发灾难性回溯,导致CPU使用率飙升至100%。本文将介绍如何使用Arthas快速定位和解决此类性能问题。
案例二:正则表达式回溯引发的CPU 100%
2. 问题现象
某日线上系统突现异常:
- 系统响应缓慢:接口响应时间显著延长,部分请求处理超时
- CPU资源耗尽:服务器CPU使用率持续维持在100%,Load值飙升
- 线程阻塞:部分线程长时间处于RUNNABLE状态,但无死锁迹象
- GC无异常:GC无明显增加,堆内存无泄漏
提示:线上问题由XSS过滤器的正则表达式引发,本文使用测试代码模拟相同场景
3. 排查过程
3.1 系统资源监控
通过top
命令快速锁定异常进程:
top - 16:34:15 up 1 day, 8:46, 3 users, load average: 0.40, 0.83, 0.78
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
16114 root 20 0 3529992 398948 13900 S 176.2 10.3 1:38.78 java
关键发现:
- Java进程(PID=16114)CPU占用率接近100%,问题指向应用程序内部。
3.2 Arthas 接入
通过Arthas快速连接目标JVM,选择 PID 为 16114 的进程,进入 Arthas 命令行。
[root@k8s-node1 ~]# java -jar arthas-boot.jar
[INFO] JAVA_HOME: /opt/jdk1.8.0_371/jre
[INFO] arthas-boot version: 4.0.5
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 16114 boot-demo.jar
1
[INFO] arthas home: /root/.arthas/lib/4.0.5/arthas
[INFO] Try to attach process 16114
[INFO] Attach process 16114 success.
[INFO] arthas-client connect 127.0.0.1 3658,---. ,------. ,--------.,--. ,--. ,---. ,---. / O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----' wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 4.0.5
main_class boot-demo.jar
pid 16114
start_time 2025-04-27 15:59:37.340
currnt_time 2025-04-27 16:01:46.218
3.3 初步分析(dashboard)
运行 dashboard 命令,每 2 秒刷新线程和资源状态:
# 输入命令
dashboard -i 2000 # 每2秒刷新一次
输出显示:
- ID25、26线程 CPU 占用极高,持续处于 RUNNABLE 状态10分钟之久。
- 无阻塞或等待迹象,确认问题与高计算任务相关。
3.4 高 CPU 线程排查(thread -n 3)
列出 CPU 占用最高的 3 个线程:
[arthas@15488]$ thread -n 3
"http-nio-8888-exec-8" Id=25 cpuUsage=96.38% deltaTime=198ms time=41076ms RUNNABLEat java.util.regex.Pattern$CharProperty.match(Pattern.java:3790)at java.util.regex.Pattern$Curly.match(Pattern.java:4241)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)at java.util.regex.Pattern$Loop.match(Pattern.java:4799)at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)at java.util.regex.Pattern$Curly.match(Pattern.java:4248)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)at java.util.regex.Pattern$Loop.match(Pattern.java:4799)at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)##### 省略部分日志 #####at java.util.regex.Pattern$Curly.match(Pattern.java:4248)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)at java.util.regex.Pattern$Slice.match(Pattern.java:3986)at java.util.regex.Pattern$Begin.match(Pattern.java:3539)at java.util.regex.Matcher.match(Matcher.java:1270)at java.util.regex.Matcher.matches(Matcher.java:604)at java.util.regex.Pattern.matches(Pattern.java:1135)at java.lang.String.matches(String.java:2121)at com.controller.TestController.isValid(TestController.java:522)at com.controller.TestController.testReg3(TestController.java:503)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)"http-nio-8888-exec-9" Id=26 cpuUsage=94.69% deltaTime=194ms time=34853ms RUNNABLEat java.util.regex.Pattern$CharProperty.match(Pattern.java:3790)at java.util.regex.Pattern$Curly.match(Pattern.java:4241)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)at java.util.regex.Pattern$Loop.match(Pattern.java:4799)at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)at java.util.regex.Pattern$Curly.match(Pattern.java:4248)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)at java.util.regex.Pattern$Loop.match(Pattern.java:4799)at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)at java.util.regex.Pattern$Curly.match(Pattern.java:4248)at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)##### 省略部分日志 #####at java.util.regex.Matcher.match(Matcher.java:1270)at java.util.regex.Matcher.matches(Matcher.java:604)at java.util.regex.Pattern.matches(Pattern.java:1135)at java.lang.String.matches(String.java:2121)at com.controller.TestController.isValid(TestController.java:522)at com.controller.TestController.testReg3(TestController.java:503)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
关键信息:
- ID25、26线程的堆栈指向 java.util.regex.Pattern 的匹配方法。
- 调用源自
com.controller.TestController.isValid(TestController.java:522)
,疑似正则匹配问题。
3.5 输入参数分析(watch)
使用 watch
命令查看 isValid
方法的入参,确认触发 CPU 飙升的输入:
watch com.controller.TestController isValid '{params,returnObj,throwExp}' -n 5 -x 3
输出示例:
ts=2025-04-27 16:10:32.522; [cost=1.171926ms] result=@ArrayList[@Object[][@String[http://www.example.com/aaaaaaaaaab],],@Boolean[false],null,
]
发现:
- 输入字符串如 http://www.example.com/aaaaaaaaaab 较长,且不完全匹配正则。
- 长输入可能触发正则引擎的回溯。
使用 trace
进一步分析方法耗时:
[arthas@16114]$ trace com.controller.TestController isValid -n 5 --skipJDKMethod false
Press Q or Ctrl+C to abort.
Affect(class count: 1, method count: 1) cost in 186 ms, listenerId: 2
`---ts=2025-04-27 16:11:18.432;thread_name=http-nio-8888-exec-4;id=21;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@16d04d3d`---[0.76795ms] com.controller.TestController:isValid()`---[82.98% 0.637233ms] java.lang.String:matches() #522`---ts=2025-04-27 16:11:25.781;thread_name=http-nio-8888-exec-2;id=19;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@16d04d3d`---ts=2025-04-27 16:11:26.577;thread_name=http-nio-8888-exec-5;id=22;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@16d04d3d`---[2318.506752ms] com.controller.TestController:isValid()`---[100.00% 2318.4676ms] java.lang.String:matches() #522
结论:长输入(如 http://www.example.com/aaaaaaaaaab)导致正则匹配耗时显著增加,触发回溯。
4.正则表达式回溯详解
正则表达式引擎主要分为 DFA(确定型有穷自动机)和 NFA(非确定型有穷自动机)。Java 使用 NFA 引擎,匹配时通过回溯(Backtracking)尝试不同路径。回溯在匹配失败时会导致引擎反复尝试,时间复杂度可能从 O(n) 恶化到 O(2^n),引发 CPU 100% 的问题,称为“灾难性回溯”。
以下是DFA(确定型有穷自动机)和NFA(非确定型有穷自动机)的对比表格:
特性 | DFA (Deterministic Finite Automaton) | NFA (Nondeterministic Finite Automaton) |
---|---|---|
状态转移 | 每个输入符号对应唯一确定的状态转移 | 同一输入符号可能对应多个状态转移(包括ε空转移) |
回溯机制 | 无回溯(线性时间匹配) | 可能触发回溯(最坏情况下指数级时间复杂度) |
实现复杂度 | 实现简单,但状态数可能较多 | 实现复杂,但状态数通常较少 |
匹配速度 | 稳定高效(O(n)) | 通常较慢(最坏情况O(2^n)) |
内存消耗 | 较高(需预计算所有可能转移) | 较低(动态计算转移路径) |
正则引擎代表 | grep、awk等传统Unix工具 | Java(java.util.regex )、Perl、Python等现代语言正则引擎 |
构造难度 | 直接构造较困难 | 更容易从正则表达式构造 |
ε-转移(空转移) | 不允许 | 允许 |
匹配方式 | 文本主导(每一步只关注当前字符) | 正则主导(尝试所有可能的路径) |
典型应用场景 | 需要高性能匹配的场景(如网络协议解析) | 需要复杂模式匹配的场景(如文本处理) |
例子 | 匹配**a*b **的DFA只有2个状态 | 匹配**a*b **的NFA可能有多个转移路径 |
4.1 回溯机制
Java 的正则引擎基于 NFA(不确定型有穷自动机),支持回溯以尝试不同匹配路径。含嵌套量词的模式在匹配失败时,可能导致指数级的路径尝试。例如:
当你写一个正则,比如:
(a+)+
去匹配一个字符串 "aaaaac"
时,正则引擎会:
- 首先尝试把整个
"aaaaac"
都匹配到内层的a+
; - 然后整个内层
a+
被匹配为一组,外层再试图重复一次; - 但由于最后有个
"c"
,无法匹配; - 引擎就开始“回溯”——尝试将内层
a+
分得更短一点(比如只匹配一半),再看外层能否继续匹配下去; - 如果一直失败,它就会继续尝试所有可能的组合。
- 匹配 aaaaac 时,引擎会尝试所有可能的 a 分配组合(如 a/aaa、aa/aa、aaaa/ 等),导致时间复杂度从 O(n) 恶化到 O(2^n)。
这就导致大量的组合尝试,尤其在输入比较长、而正则又含有嵌套的可重复匹配(比如 (a+)+
)时,回溯的路径会呈指数级增长。
4.2 CPU 飙升原因
因为回溯次数非常多,正则引擎会尝试各种可能的组合路径,直到找到匹配,或者穷尽所有路径确认不匹配。
- 对某些输入,回溯路径可能达到 数百万甚至数十亿次;
- 每次尝试都是一次函数调用、状态切换,占用 CPU;
- 某些构造甚至可以导致拒绝服务攻击(ReDoS):通过一个恶意的长字符串,拖垮服务器。
4.3 典型危险模式
易引发回溯的正则示例:
- (a+)+:嵌套贪婪量词。
- (.+)*:模糊匹配,路径分配多样。
- ((a|aa)+):多分支嵌套。
5. 问题分析与优化
5.1 原始正则问题分析
原始正则:
^http://www\.([a-z0-9\-]+)\.com/(.+)*(.+)*.html$
问题点:
- 嵌套量词 (.+)*:模糊匹配,引发大量回溯。
- 双重 (.+)*:组合数随输入长度指数增长。
- 冗余设计:两个捕获组功能重叠。
5.2 回溯触发场景:
- 不匹配输入:如 http://www.example.com/aaa…aaa(缺少 .html),引擎会:
- 让 (.+)* 捕获所有 aaa…aaa。
- 发现末尾不匹配 .html,回溯,尝试不同子序列分配。
- 对于长输入(例如 1000 个 a),回溯次数可能高达 2^n 级别。
- 部分匹配输入:如 http://www.example.com/aaa%25,% 不匹配 .html 的 .,但引擎仍会回溯,尝试调整 (.+)* 的分配。
- 长输入:如 http://www.example.com/ + 10000 个 a,回溯路径极多,可能导致程序卡死。
5.2 正则优化方案
优化后:
^http://www\.([a-z0-9\-]+)\.com/([a-zA-Z0-9_/]+)\.html$
改进措施:
- 替换模糊匹配:用 [a-zA-Z0-9_/]+ 限制字符集。
- 简化结构:移除多余捕获组。
- 强制非空:用 + 替代 *。
5.3 优化效果对比
- 性能:优化后匹配时间 <1ms,原始正则耗时数秒。
- 功能:仍支持如 http://www.example.com/path.html。
- 安全性:避免长输入引发的性能问题。
6. 进一步优化建议
-
避免嵌套的可重复匹配,比如不要用
(a+)+
; -
使用懒惰匹配(
+?
、*?
),限制贪婪; -
更严格的路径字符集:
- 如果路径只允许小写字母和数字:([a-z0-9/]+)。
- 如果支持更多字符(如连字符或点号):([a-zA-Z0-9_/.-]+)。
-
路径长度限制:
- 添加量词限制,如 ([a-zA-Z0-9_/]{1,100}),限定路径长度 1-100 字符。
- 防止超长路径影响性能。
-
支持其他协议或域名:
- 扩展前缀为 ^(http|https)😕/([a-z0-9-]+).com/,支持 https 或其他域名。
- 示例:^(http|https)😕/([a-z0-9-]+).com/([a-zA-Z0-9_/]+).html$
-
防御性编程
-
输入预处理机制
public boolean isValidUrl(String url) { // 长度校验前置if(url.length() > 1024) return false;// 关键后缀快速判断if(!url.endsWith(".html")) return false;// 执行正则匹配return url.matches(optimizedRegex); }
-
正则编译缓存
private static final Pattern URL_PATTERN = Pattern.compile("^http://www\\.([a-z0-9-]+)\\.com/([a-zA-Z0-9_/-]+)\\.html$");public boolean isValidUrl(String url) {return URL_PATTERN.matcher(url).matches(); }
-
-
测试和分析:对关键正则表达式进行性能测试,确保不会触发灾难性回溯。
-
监控告警配置
- 方法耗时监控:对isValidUrl方法设置500ms超时阈值
- 线程堆栈监控:发现大量线程卡在java.util.regex时触发告警
使用工具(如regex101.com)分析正则表达式的回溯行为。
附录:系列目录
- JVM生产环境问题定位与解决实战(一):掌握jps、jmap、jstat、jstack、jcmd等基础工具
- JVM生产环境问题定位与解决实战(二):JConsole、VisualVM到MAT的高级应用
- JVM生产环境问题定位与解决实战(三):揭秘Java飞行记录器(JFR)的强大功能
- JVM生产环境问题定位与解决实战(四):使用JMC进行JFR性能分析指南
- JVM生产环境问题定位与解决实战(五):Arthas——不可错过的故障诊断利器
- JVM生产环境问题定位与解决实战(六):总结篇——问题定位思路与工具选择策略
- JVM 生产环境问题定位与解决实战(七):实战篇——OSSClient泄漏引发的FullGC风暴
- ➡️ 当前:JVM 生产环境问题定位与解决实战(八):实战篇——正则表达式回溯导致的 CPU 100%
🔥 下篇预告:《JVM 生产环境问题定位与解决实战(九):实战篇——JVM 内存区域分配不合理导致的频繁 Full GC》
🚀 关注作者,获取实时更新通知!有问题欢迎在评论区交流讨论~