Java 字符串驻留 intern()方法详解(Java 7+)

134 阅读4分钟

Java 字符串驻留(intern())方法详解(Java 7+)

简单来说:

如果常量池中有该字符串就返回的是常量池中的对象引用,如果没有的话,返回这个堆对象本身的引用,同时常量池中存储堆对象的引用(指向堆对象)。

在 Java 7+ 中, intern() 的行为是:


如果池中没有等值字符串:

1.常量池中存储堆对象的引用(指向堆对象)

2.返回这个堆对象自身(相当于返回它本身的引用)

3.后续访问该字面值时,返回同一个堆对象引用

如果池中已有等值字符串:

1.不改变常量池的内容

2.返回池中原有的对象引用

3.堆对象不会被加入常量池

intern() 方法的核心行为(双模式)

intern() 方法的行为完全取决于调用时刻,字符串常量池中是否已存在与之 equals() 相等的字符串对象:


模式 1:常量池中不存在等值字符串时

 String s1 = new String("A") + new String("B");   
 // 1. 在堆上创建新对象 "AB",池中尚无 "AB"  
   
 String s2 = s1.intern();   
 // 2. s1调用intern(): 池中无"AB",将s1的堆引用(指s1指向的堆对象)放入池,并返回此引用  
   
 String s3 = "AB";   
 // 3. 字面量创建:发现池中已有引用,直接返回该引用 (即s1的堆引用)  
   
 System.out.println(s1 == s2); // true  
 System.out.println(s1 == s3); // true  
 System.out.println(s2 == s3); // true  

结果s1, s2, s3 全部引用同一个堆对象。intern() 将堆对象的引用添加到常量池。


模式 2:常量池中已存在等值字符串时

 String preExisting = "AB";   
 // 0. 类加载时,"AB"字面量存入常量池 (假设)  
   
 String s1 = new String("AB");   
 // 1. new 在堆上创建全新对象 "AB",池中已存在 "AB"  
 // 注意: new String("AB") 的构造参数 "AB" 本身也在池中  
   
 String s2 = s1.intern();   
 // 2. s1调用intern(): 池中已有"AB",返回池中的对象引用  
   
 String s3 = "AB";   
 // 3. 字面量创建:返回池中的对象引用  
   
 System.out.println(s1 == s2); // false  
 System.out.println(s1 == s3); // false  
 System.out.println(s2 == s3); // true  

结果

  • s1 != s2 (s1 是堆对象,s2 是池对象)
  • s2 == s3 (都为常量池对象)
  • s1 != s3

行为对比表

条件intern() 行为返回对象常量池变化
常量池中不存在等值字符串将当前堆对象引用添加到常量池当前堆对象自身新增一个引用映射
常量池中已存在等值字符串不改变常量池,返回已有对象引用常量池中的对象无变化

关键理解点

字面量 vs 运行时创建

  • 字面量(如 "AB"):

    • 类加载时加入常量池
  • new String("literal")

    • 参数 "literal" 仍是池中对象
    • 总会在堆上新建对象,引用不同
  • 拼接(如 str1 + str2):

    • 运行时创建堆对象,除非编译期常量

首次出现原则

intern() 是否将堆对象引用放入池,仅取决于调用时刻池中是否存在等值对象:

实验 1:首次出现
 String a = new String("Hel") + "lo";   
 // 运行时创建堆对象 "Hello"  
   
 a.intern();   
 // 首次出现!将a的引用存入池  
   
 String b = "Hello";   
 // 获取池中的引用 (即a的堆引用)  
   
 System.out.println(a == b); // true   
实验 2:已存在
 String pre = "Hello";   
 // 类加载时常量池已有 "Hello"  
   
 String c = new String("Hel") + "lo";   
 // 堆中创建新对象 "Hello"  
   
 c.intern();   
 // 返回池中已有引用 (忽略堆对象)  
   
 String d = "Hello";   
 // 返回池中原有引用  
   
 System.out.println(c == d); // false   
 System.out.println(pre == d); // true  

最佳实践与注意事项

避免混用字面量和 new

 //  不推荐:创建了多余堆对象  
 String s = new String("text");  
   
 //  推荐:复用池对象  
 String s = "text";  

谨慎使用 intern()

  • 适用场景

    • 内容重复大量字符串
    • 能控制首次 intern() 时机(如系统初始化)
  • 性能风险

    • intern() 检查池需同步,存在开销
    • 大量使用可能导致堆膨胀,增加 GC 压力
    • 池中已有重复值时重复检查造成浪费
  • 替代方案

    • 使用 ConcurrentHashMap<String, String> + computeIfAbsent 实现自定义驻留池
    • 具备回收、清理等灵活性

明确版本差异

Java 版本intern() 行为
Java 6 及之前存储在 永久代(PermGen) 常量池大小固定,易 OOM
Java 7 及之后存储在 上的引用,常量池属堆的一部分 减少 OOM 风险,但仍可能导致堆压力上升

Java 7+ 的优化行为总结

在 Java 7+ 中,intern() 的行为如下:

当池中没有等值字符串时:

  • 常量池中 存储堆对象的引用
  • 返回该堆对象本身
  • 后续访问字面量会 返回同一堆对象引用

当池中已有等值字符串时:

  • 不改变常量池内容
  • 返回池中已有对象的引用
  • 堆对象 不会被加入常量池

结论

正如所指出的,intern() 的行为和 == 的比较结果完全取决于:

调用 intern() 的那一刻,常量池中是否已有等值对象。

Java 7+ 中 intern() 优化为存储堆对象引用,这使我们更需深入理解常量池与堆之间的关系。 掌握这些规则,是精确理解和预测 intern() 行为的关键。