内存泄漏高发于字符串和正则处理,因隐式引用致大对象无法回收;需警惕 substring 共享数组、静态缓存大字符串、重复编译 pattern 及 js 中带 g 标志正则的状态残留。

字符串和正则表达式处理是内存泄漏的高发场景,尤其在长期运行的服务或前端单页应用中。问题往往不显眼——没有报错,但内存占用缓慢爬升,数小时后服务变卡、GC频繁、甚至OOM。关键在于:字符串本身虽小,但不当的引用、缓存、或正则对象复用方式,会把大量字符数据“钉”在堆上无法回收。
警惕字符串构造与截取的隐式引用
Java 1.6 中 String.substring() 会共享原字符串的底层 char[] 数组,即使只取一个字符,整个原始大字符串(比如读取的 JSON 文件)仍被持有。虽然 Java 7u6 后已修复为拷贝新数组,但自定义封装类、或使用 StringBuilder.toString() 后又长期缓存其结果,仍可能意外延长大对象生命周期。检查点包括:
- 是否将从数据库/文件读取的大字符串直接放入静态 Map 或单例缓存中?
- 是否反复调用
new String(byte[], charset)创建冗余副本? - 日志框架中是否对敏感字段做脱敏时,生成了中间字符串链而未及时丢弃?
正则 Pattern 对象不是“即用即弃”
Pattern.compile() 返回的对象是线程安全且可复用的,但若每次匹配都重新编译(如写成 Pattern.compile("xxx").matcher(input).find()),不仅性能差,还会导致大量 Pattern 实例堆积——每个实例内部持有编译后的状态机、命名组映射、甚至 JIT 编译的 native code 缓存。排查建议:
- 将高频正则提取为
private static final Pattern常量; - 避免在循环内、或每次 HTTP 请求中动态拼接正则字符串再编译;
- 用 JFR 或 MAT 分析 dump,搜索
java.util.regex.Pattern实例数是否异常增长;
前端场景:正则全局标志与字符串方法的陷阱
JavaScript 中带 g(global)标志的正则对象是“有状态”的:/abc/g.exec(str) 会修改其 lastIndex。若该正则被多个函数共享或缓存,且未重置 lastIndex,可能导致后续匹配逻辑错误,更隐蔽的是:某些引擎(如旧版 V8)会为带 g 的正则保留额外上下文对象,与字符串形成隐式引用链。常见风险点:
- 将
RegExp实例作为模块级变量导出并跨组件复用; - 用
String.prototype.matchAll()后未消费完迭代器,导致引擎保留引用; - 在事件回调中创建正则并绑定到 DOM 元素的 dataset 或自定义属性上,组件卸载后引用残留。
统一排查路径:从监控到快照
不依赖猜测,走标准化流程:
- 先观察:用 Prometheus + JVM Exporter 监控
jvm_memory_used_bytes{area="heap"}曲线,确认是否持续上升; - 再采样:在疑似泄漏前后各执行一次
jmap -dump:format=b,file=heap1.hprof <pid></pid>和heap2.hprof; - 后对比:用 Eclipse MAT 打开两个快照 → “Compare Basket”,聚焦
java.lang.String、char[]、java.util.regex.Pattern的实例数与 retained heap 差值; - 最后追溯:右键可疑对象 → “Path to GC Roots” → 看是谁在强引用它(常是静态容器、未清理的监听器、或缓存 Map 的 value)。











