String的intern方法详解

String.intern()

第一节 简介

  String.intern()native修饰的非Java实现函数。

1
public native String intern();

  intern方法返回一个字符串常量池中的固定对象,当调用此方法时若字符串常量池以经包含了此String对象(JDK1.7以后字符串常量池移至堆内,也就不必存储String实例了),则直接返回此对象;否则向字符串常量池中添加此对象并返回其引用。

  所以intern方法可以用来手动向字符串常量池中添加字面量。而字符串常量池其实就是一个固定大小的HashTable,当String对象创建过多时就会导致intern方法性能下降。


第二节 案例分析

实例1

  测试代码如下。

1
2
3
4
5
6
7
8
9
10
String s1 = "abc";
String s2 = new String("abc");
s2.intern();
String s3 = "abc";
printAddresses("s1",s1);
printAddresses("s2",s2);
printAddresses("s2.intern()",s2.intern());
printAddresses("s3",s3);
System.out.println("s1 == s2 : " + (s1 == s2));
System.out.println("s1 == s3 : " + (s1 == s3));

  运行结果如下。

1
2
3
4
5
6
s1: 0x463bfa700
s2: 0x463bfa880
s2.intern(): 0x463bfa700
s3: 0x463bfa700
s1 == s2 : false
s1 == s3 : true

  String s1 = "abc"; 编译阶段,根据字面量 "abc" ,查找字符串常量池内未找到,在堆中新建String实例,将其引用地址驻留字符串常量池,在运行时返回其引用地址给局部变量s1。

  String s2 = new String("abc"); new关键字在Heap中新申请一块内存区域,新建一个对象,并返回其引用。

  s2.intern() 因为字符串常量池已有字面量 ”abc” 的引用,所以返回的地址仍是 0x001

  String s3 = "abc"; 查询字符串常量池,已有字面量 "abc" ,所以直接调用其引用。

  那么是不是先用 new 创建,再调用 intern 将其驻入字符串常量池,之后的直接赋值就会相等呢?


实例2

测试代码:

1
2
3
4
5
6
7
String s1 = new String("abc");
s1.intern();
String s2 = "abc";
printAddresses("s1",s1);
printAddresses("s1.intern()",s1.intern());
printAddresses("s2",s2);
System.out.println("s1 == s2 : " + (s1 == s2));

运行结果:

1
2
3
4
s1: 0x463d2ac00
s1.intern(): 0x463d2acc0
s2: 0x463d2acc0
s1 == s2 : false

  但结果并非如此,原因就是在执行代码前,类的加载过程就已经把字面量 ”abc” 扣留到字符串常量池中了,所以第一行代码执行后应该在堆内有两个String实例,s2赋值时直接复制字符串常量池内的引用即可。


实例3

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
String s1 = new String("a") + new String("b");
s1.intern();
String s2 = "ab";
printAddresses("s1",s1);
printAddresses("s1.intern()",s1.intern());
printAddresses("s2",s2);
System.out.println(s1 == s2);

String s3 = new String("c") + new String("d");
String s4 = "cd";
s3.intern();
printAddresses("s3",s3);
printAddresses("s4",s4);
printAddresses("s3.intern()",s3.intern());
System.out.println(s3 == s4);
String s5 = "e" + "f";
String s6 = "ef";
s5.intern();
printAddresses("s5",s5);
printAddresses("s6",s6);
printAddresses("s5.intern()",s5.intern());
System.out.println(s5 == s6);

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
s1: 0x46353a600
s1.intern(): 0x46353a600
s2: 0x46353a600
true
s3: 0x463742d80
s4: 0x463742e40
s3.intern(): 0x463742e40
false
s5: 0x463745140
s6: 0x463745140
s5.intern(): 0x463745140
true

  为什么字符串拼接时,又和之前的情况不同呢?

  详细分析可以看我的这篇博客String的intern方法测试实例分析

  根据分析过程可以了解到,拼接字符串时,String Table内还未驻留最终的字符串引用,而intern先执行时就会把当前对象的引用复制给String Table,所以结果1就是相等。而字符串常量赋值先执行时,就会新建一个String对象,并驻留String Table,导致结果2不相等,而结果三 ”e“ + ”f” 对于虚拟机来说就是一个字符串常量。


实例4

测试代码:

1
2
3
4
5
6
7
public static void main(String[] args){
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);

String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}

运行结果:

1
2
true
false

  为什么str1和str2结果不同?

  Java标准库内在JVM启动时被加载的部分里,有可能有对字面量 ”java” 的引用。所以String Pool字符串常量池内应该已经加载过 ”java”

  R大的测试是 sun.misc.Version 这个类在JDK类库初始化时被加载,其初始化过程中需要对静态常量字段根据给定的常量值做默认初始化,此时静态常量字段launcher引用了 ”java” 字段被intern到HotSpot VM的字符串常量池里了。

1
private static final String launcher_name = "java";

参考博客和文章书籍等:

《Java语言规范》

《Java虚拟机规范》
如何理解《深入理解java虚拟机》第二版中对String.intern()方法的讲解中所举的例子

Java二进制指令代码解析

JAVA中String的深入研究

Java-String.intern的深入研究

运行时常量池中的符号引用/String.intern() /ldc指令

ghsea

深入解析String#intern

因博客主等未标明不可引用,若部分内容涉及侵权请及时告知,我会尽快修改和删除相关内容