Skip to content

字符串常量池 ​

String Pool(字符串常量池)是 String#intern()、new String("a") == "a" 这类面试题的总源头,也是「JDK7 常量池从 PermGen 搬到堆」这个经典演进的落点。本篇把池的动机、进池时机、位置演进、内部实现、观察手段与性能边界一次讲透,最后回答一个实战问题:要不要自建字符串池。内存区域背景见 Java内存结构,Metaspace 细节见 堆外内存Metaspace深入理解,弱引用语义见 强引用、软引用、弱引用、虚引用。

1 为什么要有字符串常量池 ​

构造一个 Java 对象不便宜,还要占用 GC 资源,缓存池就是为了缓解这个问题。Java 里早有先例——基本类型包装类的缓存:

类型缓存区间
Integer / Long / Short-128 ~ 127(Integer 可用 -XX:AutoBoxCacheMax 或 java.lang.Integer.IntegerCache.high 扩大)
Byte全部(-128 ~ 127)
Character0 ~ 127
Float / Double无(浮点值无限多,缓存无意义)

String 不是基本类型,但使用频率极高:假设 "Java" 这个字面量在应用里出现一百次,我们不希望堆里因此躺着一百个等值对象。String Pool 就是 JVM 层面的全局去重缓存。

能这样缓存的前提是不可变:Integer、Long、String 全是不可变类,共享是安全的。这也是「String 为什么设计成 final」的标准答案素材之一。

实现上,包装类缓存用的是数组;String Pool 用的是 JVM 内部的 HashTable——数组 + 链表的结构,和我们平时用的 HashMap 类似,但不可动态扩容,这一点决定了它后面所有的性能话题。

2 什么时候进池、什么时候出池 ​

两条进池路径:

  • 字面量:代码里用双引号写出的字符串,随类的加载进入 String Pool(严谨说这是 lazy 的,HotSpot 在首次使用时才解析进池,不是类加载瞬间全部驻入);
  • String#intern():池中没有则登记一条记录并返回池中引用;已有则直接返回池中引用。
java
String a = "javadoop";              // 字面量,进池
String b = new String("javadoop");  // 在堆中新建对象,"javadoop" 本身已在池中
String c = b.intern();              // 返回池中引用

a == b      // false:b 是堆中新对象
a == c      // true:c 取的是池中引用

出池没有特殊通道:只要池中的 String 对象对 GC Roots 不可达,就可以被回收——池持有的是引用,不是免死金牌。但要注意反面:池中对象过多时 YGC 会变长,因为 YGC 需要扫描 String Table;历史上就有 String.intern() 滥用导致 YGC 时间持续上涨的著名案例。

3 池在哪:跟着 JDK 版本搬家 ​

位置演进是面试最爱考的点,也是最容易答错的点:

  • Java 6 及以前:String Pool 在 PermGen 中。PermGen 大小固定(-XX:MaxPermSize=N),设多大都是死的,大量 intern 直接可能 OutOfMemoryError: PermGen,所以那个年代的调优建议是「小心使用 intern」;
  • Java 7 起:String Pool 移到堆中。堆虽然也固定大小,但调优时只需统筹调堆,池的生死和普通对象一样交给 GC;
  • Java 8:PermGen 被彻底移除、Metaspace 上位。注意:String Pool 留在堆里,没有跟着去 Metaspace——这是高频错误答案。Metaspace 只放 class metadata。

顺带一提,因为池从 Java 7 起在堆中,intern() 对首次出现的字符串可以直接记录堆中现有对象的引用,不必像 Java 6 那样往 PermGen 复制一份——同一份代码在 6 与 7+ 上的 == 判定结果可能不同,这是 intern 类笔试题的分水岭。

4 内部实现:一张不许扩容的哈希表 ​

String Pool 的核心是一张固定 bucket 数的 HashTable:

  • Java 6 早期 bucket 数固定 1009;后来可用 -XX:StringTableSize=N 配置;
  • Java 7u40 起默认提升为 60013,Java 8 沿用,对绝大多数应用够用;
  • 1009、60013 都是质数——质数个 bucket 更容易散列均匀;
  • 表不可扩容,但会 rehash:JVM 发现散列严重不均时会重排,链长随之下降。

不可扩容意味着:若代码大量调用 intern() 而表偏小,单 bucket 链表会变长,读写退化成 O(链长)。所以「会在运行期产生海量不同字符串并 intern」的应用(如动态拼出的 key),才需要手动调大 StringTableSize。

5 观察与压测:链长决定性能 ​

JVM 提供 -XX:+PrintStringTableStatistics,在进程退出时打印池统计(没有运行期实时查询的官方接口):

StringTable statistics:
Number of buckets       :   60003 =    480024 bytes, avg   8.000
Number of entries       :   4000774 =  96018576 bytes, avg  24.000
Number of literals      :   4000774 = 1055252184 bytes, avg 263.762
Total footprint         :           = 1151750784 bytes
Average bucket size     :      66.676
Maximum bucket size     :      84

关注两行:Average bucket size(平均链长)与 Maximum bucket size(最长链)。上面这份是往池里 intern 了 400 万长字符串后的数据,平均链长 66.7。

压测结论(400 万长字符串 intern,2G 堆):

bash
java -Xms2g -Xmx2g com.example.StringTest
java -Xms2g -Xmx2g -XX:StringTableSize=400031 com.example.StringTest

把表扩大到 40 万 bucket 后平均链长降到 10 左右,插入耗时几乎无损——String Pool 的性能就是链表长度的性能。链长在个位数时不用折腾,默认 60013 够用;确有海量 intern 场景再调。

6 要不要自建 String Pool:不要 ​

有人担心 JVM 池不可控,想用 HashMap 自建。正确姿势必须上弱引用,否则池本身就造成泄漏:

java
private static final WeakHashMap<String, WeakReference<String>> pool
        = new WeakHashMap<>(1024);

private static String manualIntern(final String str) {
    final WeakReference<String> cached = pool.get(str);
    if (cached != null) {
        final String value = cached.get();
        if (value != null) {
            return value;
        }
    }
    pool.put(str, new WeakReference<>(str));
    return str;
}

同样 2G 堆持续灌数据:String#intern() 大约撑到 3000 万条才开始大量 Full GC;自建的 manualIntern 到 1400 万条就先撑不住了。结论很干脆:不要自建 String Pool——JVM 内置实现更省、更快、还能被 GC 正确感知,自建没有任何收益。

面试追问链 ​

  • new String("abc") 创建了几个对象?→ 最多两个:堆中的新对象 + 「abc」若尚未进池则随字面量进池的那个引用对象;
  • intern() 的 JDK6/7+ 差异?→ 6 在 PermGen 里复制一份并返回;7+ 池在堆中,可直接记录首次出现的堆对象引用,== 结果可能不同;
  • 常量池在 Metaspace 里吗?→ 不在。Java 7 起就在堆中,Java 8 之后也没动,Metaspace 只放类元数据;
  • StringTableSize 是什么?→ 池的 bucket 数,Java 6 是 1009,7u40+ 默认 60013,都取质数;只扩 bucket 数、不动态扩容但会 rehash;
  • intern() 会引发什么 GC 问题?→ 池中对象多则 YGC 需扫描 String Table,YGC 时长被拉高;
  • 为什么 String 能被池化?→ 不可变,共享无副作用;这也回答了「String 为什么 final」;
  • 怎么观察池的健康度?→ -XX:+PrintStringTableStatistics 看退出时的平均/最大链长,链长个位数即健康。

面试官视角小结 ​

这道题的区分度在于「演进」二字。只背「常量池存字符串」是末位;能说出「Java 6 在 PermGen、7 搬进堆、8 之后也没去 Metaspace」已属合格;再补上「池是固定 bucket 的 HashTable、性能看链长、60013 是质数」就开始超出预期;若能主动聊到「intern 滥用拉长 YGC」的真实案例与「不要自建池」的实测结论,就是实战型信号了。最差的回答是把常量池说进 Metaspace,或认为池中对象永不回收——前者说明没跟踪过 JDK 演进,后者说明没理解 GC Roots。可与 强引用、软引用、弱引用、虚引用 的弱引用语义互相印证着答。

来源:javadoop《Java 字符串常量池介绍》(https://javadoop.com/post/string)——本文为研读消化后的重写笔记,原文版权归 javadoop,引用请遵原文。注:原文评论区勘误过「Java 8 常量池迁至 Metaspace」的早期表述,本文按勘误后的正确口径(Java 7 起在堆、8 后仍留堆未去 Metaspace)撰写,见正文第 2 节。