# 5. 为什么 String.intern() 在 JDK 6 和 JDK 7+ 的行为不同?

# 标准答案

String.intern() 的行为变化主要是由于 运行时常量池(Runtime Constant Pool)的位置变化。在 JDK 6 及以前,常量池存储在 永久代(PermGen),空间有限,容易导致 OOM。而在 JDK 7 及以后,常量池被移动到 堆(Heap),这样 intern() 的字符串可以和普通 Java 对象共享内存,避免了 OOM,并提高了内存利用率。

# 答案解析

要理解 String.intern() 的行为变化,需要先了解 运行时常量池的存储位置

# JDK 6 及以前:运行时常量池位于永久代(PermGen)

  1. 运行时常量池(包含 intern() 字符串)存储在 方法区(永久代)。
  2. String.intern() 机制
    • 如果字符串 已存在 在常量池中,返回已有的引用;
    • 如果字符串 不存在,则 将其拷贝到常量池,并返回引用。
  3. 问题
    • 永久代大小受 -XX:MaxPermSize 限制,过多的 intern() 调用容易导致 OutOfMemoryError
    • intern() 会创建额外的字符串副本,导致额外的内存消耗。

示例代码(JDK 6):

String s1 = new String("hello").intern();
String s2 = new String("hello").intern();
System.out.println(s1 == s2); // true

String s3 = new String("long_string_value").intern();
String s4 = new String("long_string_value").intern();
System.out.println(s3 == s4); // false(字符串过长,可能未存入常量池)
1
2
3
4
5
6
7

原因
在 JDK 6,短字符串 可以存入永久代,但长字符串可能不会,导致 intern() 返回的引用不同。

# JDK 7+:运行时常量池迁移到堆(Heap)

  1. 常量池不再位于永久代,而是存放在堆内存(Heap)。
  2. String.intern() 机制变化
    • 如果字符串已存在,返回已有的引用;
    • 如果字符串不存在,则 只存储字符串的引用,而不拷贝内容
  3. 优化点
    • 由于常量池在堆中,intern() 不会创建字符串副本,减少额外的内存占用。
    • 运行时常量池大小不再受 MaxPermSize 限制,避免 PermGen OOM

示例代码(JDK 7+):

String s1 = new String("hello").intern();
String s2 = "hello";
System.out.println(s1 == s2); // true

String s3 = new String("long_string_value").intern();
String s4 = "long_string_value";
System.out.println(s3 == s4); // true(JDK 7+,长字符串也可存入常量池)
1
2
3
4
5
6
7

原因
在 JDK 7+,intern() 只存储引用,不复制内容,因此长字符串也能正确返回相同引用。

# 常见错误与误区

误区 1:JDK 7+ 的 intern() 总是返回堆中的引用
错误intern() 只是将字符串引用存入常量池,并不意味着字符串对象一定在堆中。
正确:如果字符串 原本就在常量池(如 "hello" 这种字面量),intern() 仍然返回常量池中的引用。

误区 2:JDK 6 和 JDK 7+ 的 intern() 行为完全相反
错误intern() 的本质逻辑没变,仍然是查找已有字符串,或者存入池中。
正确:主要区别在于 存储位置变化(永久代 → 堆),以及 是否创建字符串副本

误区 3:JDK 7+ intern() 解决了所有 OOM 问题
错误:虽然 intern() 避免了 PermGen OOM,但如果堆过小,仍可能出现 Heap OOM
正确:应该监控 Heap,合理配置 -Xmx

# 最佳实践

  1. 避免过度使用 intern()

    • intern() 适用于 大量重复字符串(如枚举、日志标签等)。
    • 不要对 短期字符串 使用 intern(),否则可能占用过多堆空间。
  2. 合理配置堆内存

    • 使用 -Xmx 适当增加堆大小,防止 Heap OOM
    • 结合 jmap -histo 查看 interned 字符串占用情况
  3. 监控字符串池使用情况

    • jcmd <pid> VM.stringtable 查看字符串池大小。
    • jstat -gc <pid> 监控 GC 频率,防止 intern() 影响 GC。

# 深入追问

为什么 JDK 6 intern() 可能导致 OOM,而 JDK 7+ 不会?
JDK 6 存储字符串副本永久代(PermGen),空间有限,容易 OOM。JDK 7+ 只存储引用,并且常量池移动到了堆,避免 PermGen OOM

在 JDK 7+,如何高效管理 intern() 产生的字符串?

  • 使用 WeakHashMap 维护缓存,避免过多 intern() 导致堆膨胀。
  • 监控 GC 日志,如果 intern() 造成 GC 频率过高,需要优化堆大小。

如何快速判断 intern() 是否生效?

  • 运行 jcmd <pid> VM.stringtable,查看 interned strings 统计。
  • 观察 s1.intern() == s2 结果是否为 true

# 相关面试题

  1. JVM 运行时常量池的作用是什么?
  2. JDK 6 和 JDK 7+ 的 intern() 行为有何区别?
  3. 为什么 JDK 7+ 运行时常量池存放在堆中?
  4. 如何优化 String.intern() 使用,防止 OOM?
  5. 如何查看 Java 字符串常量池的内容?