String.intern() 不总能省内存,仅在动态生成的高重复字符串场景下有效,但有GC压力、哈希冲突和锁开销;多数场景推荐用HashSet等集合去重,更可控、安全。

String.intern() 真的能帮你省内存吗?
不能一概而论。它只在字符串内容重复率高、且这些字符串由 new String() 或非字面量方式构造时,才可能节省堆内存;但会把字符串引用转移到常量池(JDK 7+ 是堆内元空间/字符串表),带来 GC 压力和哈希冲突风险。
常见错误现象:OutOfMemoryError: Metaspace(JDK 8)或 GC 频繁(JDK 7+ 堆内字符串表膨胀);intern() 后的字符串比较用 == 仍返回 false(比如和字面量混用但没注意加载顺序)。
- 只对运行期动态拼接、解析生成的重复字符串有效,比如 JSON 解析出的字段名、日志中的固定状态码
- 字面量字符串(如
"OK")本身就在常量池,再调intern()是空操作 -
intern()是 native 方法,有锁开销,高并发下调用需权衡 - JDK 7 之前 intern 到永久代,极易 OOM;JDK 7+ 默认在堆,但需通过
-XX:StringTableSize调优,默认仅 60013 桶,冲突多时查找变慢
用 Set 去重比 intern() 更稳当?
是的,绝大多数场景下更推荐。集合去重逻辑清晰、可控、无隐式内存迁移,适合业务层统一管理重复字符串生命周期。
使用场景:读取大量 CSV/JSON 数据做 dedup,缓存 key 归一化,用户输入标签清洗等。
- 用
HashSet依赖String.hashCode()和equals(),不依赖 JVM 内部字符串表 - 若需保留插入顺序,选
LinkedHashSet;若后续要排序,直接上TreeSet(但注意String自然序可能不符合业务语义) - 注意:
HashSet的初始容量别设太小,否则扩容 + rehash 成本高;预估重复率高时,可设大些,比如new HashSet(10000, 0.75f) - 不要为了“省一个对象”在循环里反复
set.add(str.intern())——这既没省堆内存(intern()返回的是常量池引用,原对象还在),又白耗 CPU
StringTableSize 不调,intern() 就像抽奖
默认 StringTableSize=60013,是个质数,但如果你有 10 万个高频重复字符串要 intern,哈希桶严重不足,链表退化成线性查找,性能断崖下跌。
错误现象:intern() 耗时从纳秒级升到微秒甚至毫秒级;jstack 看到多个线程卡在 StringTable::intern 的 synchronized 块里。
- 估算峰值唯一字符串数,设为
-XX:StringTableSize的 2–3 倍(例如预估 8w,就设 262144) - 该参数必须在 JVM 启动时指定,运行中不可改
- 增大后内存占用会上升(每个桶存一个指针,64 位下约 8 字节 × 桶数),但换来的是 O(1) 查找均摊成本
- 可通过
jstat -gc观察CCSC(Compressed Class Space)或老年代增长趋势,判断是否真压到了元空间/堆
集合去重时,要不要先 intern() 再 add?
没必要,而且容易引入 bug。集合本身已靠 equals() 去重,再加一层 intern() 属于叠甲过度。
典型翻车点:某次上线后发现部分字符串去重失效,查下来是因为部分数据来自字面量(自动入池),部分来自 new String(buf) 后又 intern(),但另一批没 intern,导致同一内容在集合里存了两份。
- 统一走
Set.add(str)即可,语义明确,行为可预测 - 如果真想复用字符串对象,应全链路控制构造方式(比如用 builder 模式封装字符串创建逻辑),而不是在集合入口打补丁
- 极端内存敏感场景(如嵌入式 Java 或百万级短字符串缓存),才考虑用
ConcurrentHashMap.newKeySet()+ 手动intern(),但务必配套单元测试验证去重率和 GC 表现
真正难的不是选 intern() 还是 Set,而是搞清字符串来源、生命周期和复用边界——这些东西藏在业务代码深处,不会报错,但会让内存分析工具看起来像在解谜。










