字符串常量池与 String.intern()
一、概述
Java 中 String 是最常用的类之一。为了提高内存利用率和减少重复对象,JVM 设计了字符串常量池(String Pool),让相同的字符串字面量共享同一个对象。
二、字符串的三种创建方式
1 | // 方式 1:字面量 — 自动放入常量池 |
三、String.intern() 的原理
intern() 方法的行为是:检查字符串常量池中是否已经存在内容相同的字符串,若存在则直接返回常量池中的引用;若不存在,则在 JDK 7 之前将当前字符串对象拷贝一份放入常量池,在 JDK 7 之后则将当前堆中对象的引用放入常量池。
1 | // JDK 7+ intern() 行为 |
四、常量池的存储位置演变
字符串常量池的物理存储位置在不同 JDK 版本中有过重大变化:
| JDK 版本 | 常量池位置 | 特点 |
|---|---|---|
| JDK 6 及以前 | PermGen(永久代) | 大小固定(-XX:MaxPermSize),大量 intern 可能导致 OutOfMemoryError: PermGen space |
| JDK 7 | Heap(堆) | 移入堆中以解决 PermGen OOM 问题,可获得 GC 管理的好处 |
| JDK 8+ | Heap(堆)的 MetaSpace 之外 | PermGen 被 MetaSpace 取代,但 String Pool 仍在堆中 |
由于常量池存储在堆中(JDK 7+),它的容量受堆内存大小限制。如果大量调用 intern(),HotSpot 会自动调整常量池的哈希表大小以避免性能退化。
五、实际案例:使用 intern() 优化内存
假设一个服务需要处理大量的字符串,其中存在大量重复值:
1 | // 不使用 intern():100万个 "ERROR" 字符串 = 100万个对象 ≈ 40MB |
适用于:大量重复的枚举值、协议头、日志级别等。不适合:每个字符串都是唯一的场景(intern() 查找会增加 CPU 开销且无内存收益)。
六、包装类的内部缓存
与 String Pool 类似,Java 的基本类型包装类也有内部缓存:
1 | Integer a = 127; Integer b = 127; |
这个缓存机制是 JVM 规范明确规定的一部分,其范围可以通过 JVM 参数调整(例如 -XX:AutoBoxCacheMax=2000 可以将 Integer 缓存上限扩展到 2000)。
七、小结
字符串常量池的核心价值在于”共享”——相同内容的字符串只存一份,节省内存。intern() 方法提供了手动控制这种共享的能力,配合 JDK 7 之后常量池移入堆的变更,使得 intern 成为大规模重复字符串场景下的有效优化工具。但需注意过多调用 intern() 会增加常量池的维护开销,应该在明确的内存优化场景下使用。