运行时常量池以及String类的intern()方法

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。其中有一个区叫方法区,它与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。而在方法区中,有一个运行时常量池。我们知道,Class文件中除了有类的版本、字段、方法、接中等描述信息外,还有一项信息是常量池:用于存放编译期生成的各种字面量和符号引用。而这些字面量和符号引用会在类加载后存放到方法区的运行时常量池中。该运行时常量池的一个重要特征是具备动态性:Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。这种特性用得多的地方就是String类的intern()方法。

对于字符串常量,Java会确保其在内存中只有一份拷贝。如何理解,看下面的代码:

[code]
String str1 = "abc";
String str2 = "abc";
String str3 = "abc";
System.out.println(str1 == str2);
System.out.println(str2 == str3);
System.out.println(str1 == str3);
[/code]

对于上面的代码,其中[“abc”]便是字符串常量。于是[“abc”]在内存中只会有一份拷贝的意思就是上面代码的输出结果会是:

[code]
true
true
true
[/code]

当Java虚拟机执行String str1 = “abc”;这一行代码的时候,会去运行时常量池中找[“abc”]是否已经存在,如果存在,则会把常量池中[“abc”]的引用返回给变量str1,如果不存在则先将其放入常量池中,再返回它的引用。后面两行代码同样如此。所以str1、str2、str3他们所引用的都是同一个内存地址。事实上,因为[“abc”]是字符串常量,这在编译期就可以确定,在编译的时候,编译器就会将其放入Class文件的常量池中,而当类被加载到虚拟机中执行的时候,便会将这些常量池中的信息加载的方法区的运行时常量池中。

String类有一个构造方法:public String(String original),在Java API中对此方法的描述是:

Initializes a newly created String object so that it represents the same sequence of characters as the argument; in other words, the newly created string is a copy of the argument string.
最后半句the newly created string is a copy of the argument string.也就是说,新生成的string是参数中的String对象的一份拷贝,即与参数[original]不是同一个内存地址。如何理解请看如下代码:

[code]String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1 == str2);[/code]

Java编译器在编译上面第一行代码的时候,可以确定的是字符串[“abc”]是一个常量,于是乎,便会把其放入Class文件的常量池中,一旦类被加载到虚拟机中执行,便会将[“abc”]放入方法区的常量池中。而变量str1所指向的对象在编译期是无法确定的,只能等到运行的时候,实际执行了这段代码分配了内存区域才能确定。当Java虚拟机执行到第一行代码的时候,由前面给出的Java API中对于String类的构造函数String(String original)的说明可以得出,new String(“abc”)会返回一份[“abc”]的拷贝给str1,即,会在堆中开辟一个空间,放入[“abc”],并把引用赋给str1,不管参数[“abc”]是在堆中还是在常量池中都是如此。所以很多人说上面这一行代码会产生两个对象[“abc”],一个在运行时常量池中,一个在堆中。所以上面的代码输出的结果是

[code]false[/code]

。再看下面的代码:

[code]String str1 = "abc";
String str2 = new String("abc");
String str3 = new String(str2);
System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str2 == str3);[/code]

上面的代码输出结果为:

[code]false
false
false[/code]

String类中有一个方法intern(),Java API 中的部分说明如下:

A pool of strings, initially empty, is maintained privately by the class String.

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
意思是:有这么一个由String类维护的专门存放字符串的池,初始化的时候是空的(从虚拟机的层面看,这个池就是指的运行时常量池的一部分,为什么是一部分,因为运行时常量池可不是只用来存放字符串常量的)。当intern()方法被调用的时候,如果这个池子中已经包含了一个与这个String对象(调用intern方法的对象)相等的字符串,则返回池子中的字符串,否则将这个String对象加到这个池子中,然后返回这个对象的引用,其中,相不相等是由equals(Object)方法来决定的。请看如下代码:

[code]String str1 = "abc";
String str2 = str1.intern();
String str3 = str2.intern();
System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str2 == str3);[/code]

上面的代码的输出结果是

[code]true
true
true[/code]