字符串常量池在jdk 6位于永久代,jdk 7移至堆中仅存引用,jdk 8+仍在堆中;运行时常量池的符号引用随类元数据进入元空间,字符串对象本体始终在堆;元空间使用本地内存,不受-xmx限制,oom类型变为metaspace。

掌握元空间变量迁移,关键不是死记版本号,而是理清“方法区实现方式变化 → 常量池归属调整 → intern行为改变”这条主线。方法区是JVM规范里的逻辑概念,而永久代和元空间只是HotSpot对它的不同物理实现。常量池本身不独立存在,它始终依附于方法区的载体。所以所谓“迁移”,本质是载体变了,池子跟着搬家。
字符串常量池(StringTable)的三次落点
这是最直接影响开发行为的部分:
-
JDK 6及之前:字符串常量池和运行时常量池都在永久代(PermGen)中。调用
String.intern()会把字符串对象(或其引用)存进永久代,容易触发java.lang.OutOfMemoryError: PermGen space; - JDK 7:字符串常量池被整体迁出永久代,移到Java堆中,但只存引用(指向堆里已存在的字符串对象),不再复制字符串内容;
-
JDK 8+(元空间时代):字符串常量池仍在堆中,未进入元空间;而运行时常量池中的符号引用部分(如类名、字段名、方法签名等)随类元数据一起进入元空间;字符串对象本体(如
"hello")始终在堆,从未进过元空间。
运行时常量池(Runtime Constant Pool)的归属逻辑
它不是整体搬迁,而是按内容拆分管理:
- 编译期生成的字面量(如数字、true/false)和符号引用,在类加载时进入运行时常量池;
- JDK 7起,字符串字面量(
"abc")对应的引用登记到堆中的StringTable,但字面量本身仍出现在类文件的常量池表中; - JDK 8后,运行时常量池的“结构信息”(如类全限定名、字段描述符)随类元数据进入元空间;真正需要GC管理的字符串引用,仍由堆中StringTable维护。
元空间不是“新堆”,而是本地内存直管区
理解这点才能避开调优误区:
- 元空间使用的是本地内存(Native Memory),不是JVM堆内存,所以不受
-Xmx限制; - 默认无上限,最大可达到系统可用内存;可通过
-XX:MaxMetaspaceSize设置硬限制(推荐设,防失控); - 元空间不会发生传统意义上的“Full GC”,但类卸载失败会导致元空间缓慢增长,最终OOM(
java.lang.OutOfMemoryError: Metaspace); - 永久代的
-XX:PermSize/-XX:MaxPermSize参数在JDK 8+已失效,强行使用会被忽略或报错。
验证迁移效果的实操建议
别只看文档,动手验证更可靠:
- 用
jstat -gc <pid></pid>观察JDK 7 vs JDK 8下永久代/元空间使用趋势; - 写一段循环调用
String.valueOf(i).intern()的代码,分别在JDK 6/7/8上运行,对比OOM错误类型(PermGen vs Heap vs Metaspace); - 用
jmap -histo:live <pid></pid>查堆内java.lang.String实例数,再用jcmd <pid> VM.native_memory summary</pid>看元空间占用,确认字符串对象与元数据分离事实; - 注意:
intern()在JDK 7+返回的是堆中已有字符串的引用,不是新建对象——这点直接影响内存泄漏排查逻辑。










