jdk 6及更早版本中substring()因共享原char[]数组且仅调整offset/count,导致小字符串长期持有大数组引用而引发内存泄漏;jdk 7u6起默认新建数组实现解耦。

Java 中 substring() 导致内存泄漏,核心问题不在“截取动作”本身,而在于它和底层字符数组的引用关系——尤其在老版本 JDK 或某些兼容环境中,小字符串可能意外持有了超大字符数组的全部内存。
看清楚你用的是哪个 substring 行为
不同 JDK 版本实现差异极大,不能只凭“用了 substring”就下结论:
- JDK 6 及更早:返回的新 String 共享原 String 的
char[],仅靠offset和count定位。哪怕只取一个字符,整个原始大数组都无法被 GC 回收。 - JDK 7u6 起(含 8/11/17/21):
substring()总是新建char[](或byte[],JDK 9+ 使用压缩字符串),子串与原串完全解耦,不再共享底层数组。 - 但注意:Android ART(5.0–7.x)、部分 OpenJDK 衍生版、或自定义 JVM 环境,仍可能沿用类似 JDK 6 的共享逻辑。
快速判断是否真有风险
别猜版本,直接看堆里对象的实际引用关系:
- 用 VisualVM / JProfiler / Eclipse MAT 抓 heap dump,找到某个小 String 实例,展开其
value字段,看对应char[]的长度是否远大于该 String 的length()—— 若是,说明仍在共享底层数组。 - 代码中临时加断点调试:
String s = "x".repeat(1000000); String sub = s.substring(10, 11); System.out.println(sub.value.length);。如果输出是 1000000,就属于不安全行为。
稳妥通用的修复写法
无论 JDK 版本如何,只要想确保子串不拖累原数组,就强制触发一次独立拷贝:
- ✅ 推荐:
new String(s.substring(start, end))—— 调用 public 构造器,一定新建字符数组。 - ❌ 避免:
s.substring(start, end).toString()—— 返回自身,不解决共享问题。 - ⚠️ 注意:
String.valueOf(s.substring(...))不起作用,它只是类型转换,不触发拷贝。
从源头规避比事后修复更有效
真正高危场景往往不是“截几个字”,而是处理大文本时无意识放大了风险:
- 不要一次性把 GB 级日志/JSON/文件内容读成
String;改用BufferedReader.readLine()流式逐行处理。 - 若必须截取大文本片段,优先用
CharBuffer.wrap(charArray, offset, length)或String.valueOf(charArray, offset, length),它们都直接操作数组,不经过 String 共享逻辑。 - 对超大字符串做
trim()、replaceAll()或正则匹配前,先用s.isEmpty() || s.chars().allMatch(Character::isWhitespace)快速预检,跳过无意义操作。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











