jdk 6中substring()因复用原char[]且仅调整offset/count,导致小字符串长期持有大数组引用;当小字符串存活而大字符串不可达时(如存入静态缓存),即发生内存泄露;new string(sub)可强制拷贝新数组断开引用链。

Java 6 中 substring() 共享 char[] 的真实表现
在 JDK 6 及更早版本中,substring() 并不拷贝底层字符数组,而是复用原 String 的 value 字段,并仅调整 offset 和 count。这意味着哪怕你只取 5 个字符:largeStr.substring(100, 105),返回的 String 对象仍强引用着整个原始 char[10_000_000] 数组。
什么场景下泄露会立刻暴露?
泄露是否实际发生,取决于“小字符串存活时间”和“大字符串是否已不可达”这两个条件同时成立:
- 从大日志、大 JSON 或大 XML 中提取 ID、状态码等短字段,并存入静态缓存(如
static Map<string object></string>) - 将
substring()结果作为 long-lived bean 的字段(如 Spring 单例中保存了某个配置片段) - 在堆 dump 中看到大量
java.lang.String实例,但其value.length远大于count(比如value.length == 2_000_000而count == 8)
为什么 new String(sub) 能破除引用链?
这个操作看似多余,实则是绕过旧版共享机制的最直接手段:
-
new String(sub)调用的是 public 构造器,它内部会调用Arrays.copyOfRange(sub.value, sub.offset, sub.offset + sub.count) - 新
String拥有自己独占的、刚好大小的char[],与原大数组彻底断开 - 只要原大字符串不再被其他地方引用,GC 就能回收整个
char[]
注意:该技巧仅在 JDK 6/7u5 及之前必要;JDK 7u6+ 已默认采用拷贝策略,new String(sub) 不再有实际收益,反而多一次数组复制。
如何快速确认当前运行环境是否存在此风险?
别猜,用工具看堆里真实结构:
- 用
jmap -dump:format=b,file=heap.hprof <pid></pid>抓堆快照 - 用 Eclipse MAT 或 VisualVM 打开,按
value.length倒序排列java.lang.String - 筛选出那些
count很小(如 ≤20)、但value.length异常大(如 ≥100_000)的实例 - 检查它们的 GC Roots 路径——如果路径终点是 static 字段或 long-lived 对象,风险坐实
真正容易被忽略的不是“会不会 leak”,而是“明明只留了 10 字符,为什么 GC 不收那 10MB”。根源不在 substring 本身,而在它无意中成了大数组的隐式持有者。










