目录
前言
相信大多数Java程序员都做过String s1 = new String("hello");创建了几个对象这种问题,并能脱口而出答案:创建了2个对象,先在字符串常量池中创建了个”hello“字符串对象,然后在堆中创建了一个字符串对象。
那么问题来了:
1、为什么要创建两个对象?这行代码究竟发生了什么?
2、字符串常量池中的内容可以在运行时添加吗?
3、随着JDK版本的变化,上述内容有什么改变吗?
如果你对上面的问题还有疑问,那么读完本文对你一定能有所帮助。
String对象的创建
我们都知道Java代码要被执行,需要先被编译成为class文件。class字节码文件(以最经典的Helloworld.class为例)的大致结构如下:
其中我们看到有一项:Constant pool,即常量池,它包括了方法符号引用、字段符号引用、类符号引用以及字符串常量这四个常量池。其中的字符串常量池就是用来记录class文件中的字面值字符串常量。由于本文说的就是字符串常量池,所以为了方便起见,下面所说的常量池默认指的就是字符串常量池。
class文件需要JVM解释成机器码才能被运行。当JVM加载class文件时,需要创建许多内存数据结构来存放class文件中包括常量池在内的字节数据。同时,JVM会自动为常量池中的字符串常量字面值创建新的String对象(intern字符串对象,又叫拘留字符串对象),然后把常量池的入口地址转变成拘留字符串的直接地址(常量池解析)。
因此,我们说String s1 = new String("hello");创建了2个对象,先在字符串常量池中创建了个”hello“字符串对象,然后在堆中创建了一个字符串对象。
String.intern()方法
既然字面值字符串常量会被直接放入常量池中,那么其他字符串也可以放进常量池吗?
这里我们就要介绍一下今天的主角:String.intern()方法。String对象的intern方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与String对象的equals结果是true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用。
Java运行过程中,所有相同字面值的字符串常量只可能建立唯一一个拘留字符串对象。
需要注意的是,从JDK7开始,字符串常量池出现了如下变化:
1、在JDK6及以前的版本,字符串常量池是在方法区(Method Area)。而从JDK7开始,字符串常量池被移动到了堆(Heap)中。
2、从JDK7开始,字符串常量池中可以不用再存储一份对象,如果堆中已经有了这个字符串对象,可以直接存储堆中这个对象的引用,不用再创建一个对象了。楼主的理解是,堆中已有的这个字符串对象就成了拘留字符串对象。
因此,由于不同JDK版本的常量池的不同,造成了intern()方法的表现存在一定的差异。
案例及说明:
我们分别在JDK6和JDK8运行下面的代码。
public static void main(String[] args) {
String s1 = new String("hello");
s1.intern();
String s2 = "hello";
System.out.println(s1 == s2);
String s3 = new String("hello") + new String("world");
s3.intern();
String s4 = "helloworld";
System.out.println(s3 == s4);
}
答案是:JDK6: false false ,JDK7: false true。
我们来解释一下在JDK6和JDK7环境各行发生了什么。我们将存在的差异和关键点用不同颜色注明,来进行比较。
JDK6:
第一行:
(1)由于JDK6的常量池在方法区中,所以JVM为字面值常量"hello"在方法区中创建了一个拘留字符串对象,并且将常量池中这个字面值指向了这个拘留字符串对象地址
(2)在堆内存中创建了一个String对象,该对象的内容指向了常量池的"hello"
(3)栈内存中引用s1指向了堆中String对象的地址。
第二行:返回拘留字符串"hello"的地址。由于常量池中已经有"hello"拘留字符串,所以直接返回(但是由于没有接收返回值,所以可以认为这行代码没有任何作用)
第三行:由于已经有了拘留字符串"hello",且相同字面值常量的拘留字符串只能有一个,所以栈内存中s2引用直接指向了拘留字符串"hello"的地址
第四行:引用类型的==比较的是引用的地址,由于方法区和堆不在一块区域,所以常量池中拘留字符串对象的地址显然与堆内存中String对象的地址不同,所以打印结果为:false
第五行:这一行相当于String s3 = new StringBuilder().append(new String("hello")).append(new String("world")).toString();
(1)这行代码中在堆里创建的new StringBuilder()、new String("hello")和new String("world")和我们要讨论的无关,这里不多说。
(2)StringBuilder拼接字符串完成后通过toString()方法在堆里创建了一个new String("helloworld")字符串对象(这里并不会放入字符串常量池),栈内存中s3引用指向了堆内存中字符串对象的地址
第六行:返回拘留字符串"helloworld"的地址。由于常量池中没有"helloworld"拘留字符串,所以在常量池创建了一个"helloworld"拘留字符串,返回这个拘留字符串地址(但没有接收返回值)
第七行:已经有了拘留字符串"helloworld",所以栈内存中s4引用直接指向了拘留字符串"helloworld"的地址
第八行:同样由于方法区和堆不在一块区域,所以常量池中拘留字符串对象的地址显然与堆内存中String对象的地址不同,所以打印结果为:false
所以JDK6的结果为false false。
JDK8:
第一行:
(1)由于JDK6的常量池在堆中,所以JVM为字面值常量"hello"在堆中创建了一个拘留字符串对象,并且将常量池中这个字面值指向了这个拘留字符串对象地址
(2)在堆内存中创建了一个String对象,该对象的内容指向了常量池的"hello"
(3)栈内存中引用s1指向了堆中String对象的地址。
第二行:返回拘留字符串"hello"的地址。由于常量池中已经有"hello"拘留字符串,所以直接返回(但是由于没有接收返回值,所以可以认为这行代码没有任何作用)
第三行:由于已经有了拘留字符串"hello",且相同字面值常量的拘留字符串只能有一个,所以栈内存中s2引用直接指向了拘留字符串"hello"的地址
第四行:引用类型的==比较的是引用的地址,由于第一行是在堆中创建了两个对象,地址不同,所以打印结果为:false
第五行:这一行相当于String s3 = new StringBuilder().append(new String("hello")).append(new String("world")).toString();
(1)这行代码中在堆里创建的new StringBuilder()、new String("hello")和new String("world")和我们要讨论的无关,这里不多说。
(2)StringBuilder拼接字符串完成后通过toString()方法在堆里创建了一个new String("helloworld")字符串对象(这里并不会放入字符串常量池),栈内存中s3引用指向了堆内存中字符串对象的地址
第六行:返回拘留字符串"helloworld"的地址。常量池中没有"helloworld"拘留字符串,但是堆中有"helloworld"对象,所以常量池中直接存储了这个对象的引用,没有另外创建对象(但没有接收返回值)
第七行:已经有了拘留字符串"helloworld",所以栈内存中s4引用直接指向了拘留字符串"helloworld"的地址
第八行:由于拘留字符串对象实际就是堆中的"helloworld"对象,所以s3与s4的指向一致,所以打印结果为:true
结语
本文主要对不同JDK版本常量池的变化,以及造成String.intern()方法的表现差异进行了说明,希望能对广大读友有所帮助~不足之处欢迎指出交流~~
最后需要感谢这几位博主大大的博文,给了我很大帮助,非常感谢~