string.intern()在循环中频繁调用会导致元空间(jdk8+)持续膨胀,因每次唯一字符串均新增常量池条目,占用本地内存而非堆内存,最终触发outofmemoryerror: metaspace。

长循环体内不当调用 String.intern() 会直接冲击方法区(JDK 8+ 为元空间)中的运行时常量池,这种膨胀不是堆内存增长,而是符号引用与字符串实例元数据在**本地内存(native memory)中持续注册导致的结构性堆积**。从 JVM 指令集视角看,关键在于 ldc、invokenative 和常量池解析行为的协同作用。
指令流如何触发常量池注册
每次执行 String.valueOf(i++).intern(),字节码层面至少包含三步核心操作:
-
ldc指令加载字符串字面量或构造临时 String 对象:例如String.valueOf(123)生成堆上新对象,不进常量池;但后续.intern()才是关键 -
invokenative调用String.intern():该 native 方法由 JVM 实现,其逻辑在 JDK 7+ 中是“若未存在,则将当前堆对象引用登记进运行时常量池” -
常量池符号解析完成时,需在本地区域分配结构体记录该引用:每个唯一字符串对应一个
vmSymbol或ConstantPool条目,含哈希桶索引、引用指针、长度等字段,占用元空间内存
为什么循环加剧膨胀而非复用
运行时常量池对 intern() 的判定依据是内容相等且首次注册,而非对象身份。在循环中:
-
String.valueOf(0).intern()→ 注册"0" -
String.valueOf(1).intern()→ 注册"1" - ……直到
"99999",每条都是新字符串字面量内容,全部被独立登记 - JDK 7/8 不再复制对象到永久代/元空间,但仍需为每个唯一值维护一个池内条目(entry),条目本身有固定开销(通常 24–32 字节),大量条目叠加即造成元空间耗尽
与堆溢出的本质区别
堆溢出(Java heap space)反映的是对象实例占满 Java 堆;而常量池膨胀表现为:
- 异常信息为
java.lang.OutOfMemoryError: Metaspace(JDK 8+)或PermGen space(JDK 7 及以前) - dump 文件中看不到大量 String 实例堆积在堆,但
jstat -gc显示M(Metaspace)使用率持续攀升 - 使用
jcmd <pid> VM.native_memory summary</pid>可观察到Internal或Class类别内存异常增长
可验证的最小复现路径
编译并运行以下代码(JDK 8u292+,限制元空间):
javac RuntimeConstantPoolOOM.javajava -XX:MaxMetaspaceSize=8m RuntimeConstantPoolOOM
其字节码中 main 方法内循环体必然含重复的 ldc + invokevirtual java/lang/String/intern 指令序列——正是这一固定模式,在每次迭代中向元空间提交新注册请求,最终越过阈值。











