JDK 6中substring()不复制char[],仅共享并偏移,导致小字符串长期持有大数组引用而引发内存泄漏;JDK 7u6起改为拷贝所需字符,彻底解决该问题。

JDK 6:substring() 不复制数组,只共享并偏移
在 JDK 6 及更早版本中,substring() 并不创建新的字符数组。它复用原字符串的 char[] value,仅通过调整 offset(起始位置)和 count(长度)来定义“视图”。
这意味着:哪怕你从一个 10MB 的字符串中截取 5 个字符,返回的子串对象仍强引用着整个 10MB 的 char[]。只要这个小子串还被持有(比如放进 static Map、单例 Bean 或 ThreadLocal),原始大数组就无法被 GC 回收——造成典型的内存泄漏。
典型风险场景包括:
- 从超大日志或 JSON 响应中提取短 ID,并缓存到静态容器
- Spring 单例中保存了某次 substring 得到的配置片段
- 堆转储中看到大量 String 实例,其 value.length 很大但 count 很小(如 value.length=2_000_000,count=8)
JDK 7u6 起:默认拷贝所需字符,彻底切断引用
从 JDK 7 update 6(2012 年发布)开始,OpenJDK 和 Oracle JDK 均重写了 substring():它会调用 Arrays.copyOfRange(),将指定范围的字符显式复制到一个全新、大小刚好的 char[] 中。
立即学习“Java免费学习笔记(深入)”;
新子串与原始字符串底层数组完全无关。即使原始字符串很大,只要没其他引用,它的 char[] 就能被正常回收。
这一改动也延续到后续所有主流版本:
- JDK 8、11、17、21 全部采用安全实现
- JDK 9+ 进一步改用 byte[] + coder,支持 Latin-1 编码压缩,内存更省、逻辑更清晰
如何验证和规避旧版风险
若仍在维护老系统(JDK 6 或 JDK 7u5 及之前),可手动破除共享:
- 用 new String(str.substring(begin, end)) 强制触发数组拷贝(构造器内部执行 copy)
- 避免将 substring 结果长期存入静态字段、单例或长生命周期容器
- 用 jmap -dump 抓堆快照,再用 MAT 按 value.length 排序,筛选出 “小 count + 大 value.length” 的 String 实例,检查 GC Roots 路径
现代开发中,只要运行环境是 JDK 8u121+ 或 JDK 11+,无需为 substring 写额外防御代码——但需确保上线环境真实符合该要求,而非仅编译用新 JDK、运行仍跑在旧 JVM 上。


















