bigywater.tech
Java · 183

正则表达式:回溯引擎为什么会灾难性回溯

正则表达式为什么会「灾难性回溯」?在 JDK 21 上哪些写法真的会爆炸?这篇用同一组输入测九种模式,再给出修复与超时的办法(Formatter 在后面的篇目)。

0 ms
(a+)+b 等贪婪写法在 JDK 21 上 n = 28 的耗时
> 2000 ms
(a+)+?b、(.*a){12}、^(a+)+\1b 在 n = 28 时的结果
4000
(a|b)* 在这个输入长度时 StackOverflowError

哪些写法会爆炸

实测 · 本机执行输出
  各模式遇到「n 个重复单元 + 一个会让匹配失败的字符 !」时 matches() 的耗时(ms,超过 2000 ms 中止记为 >2000):
    模式                         n = 16   n = 20   n = 24   n = 28
    (a+)+b                          0        0        0        0
    ^(a+)+$                         0        0        0        0
    (a|aa)+b                        0        0        0        0
    ^(?:a|ab)*$                     0        0        0        0
    (a+)+?b                         4       50      745    >2000
    (.*a){12}                       8       69      699    >2000
    ^(a+)+\1b                       4       61      978    >2000
    (a|b|ab)*?c                    17      225    >2000    >2000
    (?:x+x+)+?y                     3       38      613    >2000
  对比 a+b(n = 100000):0 ms

修复

实测 · 本机执行输出
  原模式(懒惰)                (a+)+?b        输入 24 个 a + c:   255 ms
  原子组                    (?>a+)+?b      输入 200 万个 a + c:    15 ms
  占有量词 a++               (a++)+?b       输入 200 万个 a + c:    10 ms
  改写成 a+b                a+b            输入 200 万个 a + c:    10 ms

栈溢出

实测 · 本机执行输出
  (a|b)*       输入 "abab…":最长成功的长度 2000,n = 4000 时 StackOverflowError
  (?:a|b)*     输入 "abab…":最长成功的长度 2000,n = 4000 时 StackOverflowError
  [ab]*        输入 "abab…":最长成功的长度 128000,到 128000 都没有溢出
  (a|b)*+      输入 "abab…":最长成功的长度 128000,到 128000 都没有溢出
  (?:a|b)*+    输入 "abab…":最长成功的长度 128000,到 128000 都没有溢出

截止时间

实测 · 本机执行输出
  包一层带截止时间的 CharSequence(200 ms):匹配超时,实际在 200 ms 后中止(没有它,这个输入会算非常久,我没有等它算完)

分步演示

正则回溯
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12
    • 没有验证:JDK 17 与 27 上同样模式的表现(经典写法在 JDK 21 上不爆炸的原因没有查)、java.util.regex 以外的引擎(RE2)、Pattern 的 UNICODE 等标志的影响、回溯次数的直接计数、更多的真实世界 ReDoS 模式;耗时是一次运行的结果
    京ICP备2026067502号-1京公网安备11010502063487号