JVM 调优绕不开 StringTable,尤其是准备 jvm 面试题的人,十有八九会被 intern() 和“字符串到底创建了几个对象”这类问题难住。我自己当年第一次看到 new String("java") + new String("java") 这种题目时,也是一脸懵,后来把 class 文件常量池、字节码指令、JVM 内存模型、GC 回收这几块全部串起来,才真正形成体系。StringTable 名字听起来只是一张表,但它横跨编译期、类加载期、运行期,背后还牵扯到对象创建、引用传递、内存分布和性能调优,绝不是一个可以“死记结论”的知识点。
这篇文章我打算从底层编译优化入手,先拆清楚字符串字面量从 class 文件到 StringTable 的完整路径,再深入 intern() 的核心机制,最后用几道高频面试题和实际参数调优来验证每个结论。无论你是准备 JVM 面试,还是想解决线上大量字符串导致的内存压力,这篇都可以给你一条完整可复用的分析思路。
1. StringTable 到底是个什么“表”
1.1 在 JVM 内存模型中的位置
很多人会混淆方法区、运行时常量池、字符串常量池这三者的关系。JVM 内存模型里,方法区存储类信息、常量、静态变量等数据,它是一种逻辑概念。在 JDK 6 及之前,方法区的实现是永久代(PermGen),StringTable 就躺在永久代里;JDK 7 开始,HotSpot 把 StringTable 挪到了 Java 堆;JDK 8 彻底移除永久代、引入元空间,但 StringTable 依然在堆中。
这个位置变化非常关键。永久代有固定大小限制,字符串一多就容易 OutOfMemoryError: PermGen space,而且永久代的垃圾回收条件很苛刻,字符串常量很难及时释放。搬进 Java 堆之后,StringTable 里的字符串对象可以像普通对象一样被 Young GC、Full GC 扫描和回收,内存瓶颈从“永久代不够用”变成了“堆不够用”,OOM 提示也变成了 Java heap space。
如果你去问一个准备 jvm 面试的人,StringTable 在哪,他能答出“JDK7 之前永久代、JDK7 之后堆”已经算不错。但更准确的理解是:StringTable 是一张由 JVM 内部 C++ 代码实现的哈希表,不属于任何 Java 标准库结构,你也无法通过标准 API 拿到它的直接引用。它只是 JVM 运行时维护的一张全局表,负责管理被“驻留”(intern)的字符串引用。
1.2 表里存的是引用,不是字符串内容
StringTable 的每个条目本质上是一个指向 String 对象的引用。String 对象本身在堆中分配,对象内部通过 char[](JDK 8 及以前)或 byte[](JDK 9 及以后)保存真正的字符内容。StringTable 利用字符串对象的哈希值作为 key 来计算桶位置,桶内再通过链表解决哈希冲突。
这个设计带来的一个直接收益是:如果多个变量引用同一个 String 对象,它们可以共享同一份底层字节数组,节省内存。intern() 的核心目的就是让这种情况更容易发生——尽量让相同内容的字符串在 JVM 里只有一份对象。
这里有一个常见的认识误区:有人以为 StringTable 里存的是字符串内容的字符数组,所以做 intern() 就能让所有字符串共享同一个数组。实际上不是。StringTable 存的是 String 对象引用,去重粒度是整个 String 对象,而不是内部数组。真正“共享底层数组”的优化是 JDK 9+ 的 Compact Strings 和 G1 的字符串去重,它们和 StringTable 是两套机制,后面我会单独讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 编译优化:class 文件里的字符串如何被加载
2.1 常量池里的 CONSTANT_Utf8 与 CONSTANT_String
在 Java 源码里写下一个字符串字面量,比如 "hello",编译器会把这个字符串相关信息写进 class 文件的常量池。javap -v 可以看得一清二楚:
txt复制Constant pool:
#7 = String #8
#8 = Utf8 hello
#7 是 `CONST
