系统性学习请点击jvm学习目录
字符串常量池(String pool),我们这里简称为字符串池。在java代码中,我们经常使用字符串,使用的可以说是相当频繁,所以jvm为了提高效率,并节省开销,在内存中创建了一个字符串常量池来存储String对象。这个字符串池可以看做一个集合,(同一值只能存一次)且可以多个变量引用一个String对象。这里我们要将字符串池和堆区分开(虽然它俩是包含关系)。
针对String类型对象的两种创建方式,jvm有不同的规定,从而导致我们String变量所引用的对象不一样。
String s1 = "abc";
这种字面值创建方式,jvm规定,在创建时,首先查询字符串池,如果其中有值为"abc"的String对象,那么就直接返回字符串池中值为"abc"的对象,否则,会在字符串池创建值为"abc"的对象,并返回该对象。总之,字符串池中,有则返回,无则创建返回,不管怎样,都是从字符串常量池中返回。String s1 = new String("abc");
这种new创建方式,jvm规定,首先在字符串池中查找有没有"abc"这个字符串对象,如果有,则不在池中再去创建"abc"这个对象了,直接在堆中创建一个"abc"字符串对象,然后将堆中的这个"abc"对象的地址返回;如果没有,则首先在字符串池中创建一个"abc"字符串对象,然后再在堆中创建一个"abc"字符串对象,然后将堆中这个"abc"字符串对象的地址返回。总之,字符串池中,有则不创建,无则创建,不管怎样,都是要在堆中new的String对象,都是返回堆中new的哪个对象。这里如果不想细看,只要注意我加粗的两句话就行,就能够大致把握我创建的String对象实际存放在哪。
下面来说一下字符串池在java内存中的位置。
在jdk1.8之前,字符串常量池是在运行时常量池中,而运行常量池是方法区一部分,jdk1.8之前的版本,方法区是用永久代的概念来实现的,具体位置位于堆内存中。而jdk1.8以后,字符串常量池不再存在于运行时常量池中,它直接存在于堆内存中,而方法区带着运行时常量池原理了堆内存,采用元空间来实现,它们使用的是本地内存。
字符串常量池一般是不进行垃圾回收的,因为字符串池存在的初衷就是为了提高效率,减少开销,尽量让常用的String对象能够直接使用。
上面讲了字符串常量池,我们对于String对象在java内存中的位置有了一些了解,下面就结合一些例子来分析分析,这些例子是可以作为一些简单的面试题。
例子1:字符串池中对于值相同的String对象最多存在一个,字面值创建
//采用字面值赋值
String s1 = "abc";//存在于字符串池中的对象
String s2 = "abc";//存在于字符串池中的对象
System.out.println(s1 == s2);//true
一开始,我们的字符串池为空,先在字符串池中创建“abc”这个String对象,再把这个对象返回给s1,然后在初始化s2时,发现字符串池中已经有了“abc”,那么此时直接把该对象返回给s2,所以这里我们的输出结果为true,即s1与s2指向的是同一对象。
例子2:new关键字实例化String对象
//采用new关键字创建一个字符串对象
String s3 = new String("abc");//存在于堆中的对象
String s4 = new String("abc");//存在于堆中的对象
System.out.println(s3 == s4);//false
一开始,我们的字符串池为空,初始化s3时,先在堆中new一个“abc”对象,然后查询字符串池发现没有该对象,于是放入,并将刚刚new的那个堆中的对象返回。然后在初始化s4时,同样,先又在堆中new一个“abc”对象,然后查询字符串池发现已经有了该对象,于是不操作,并将刚刚new的那个堆中的对象返回。因为s3和s4分别指向堆中的一个对象,所以肯定内存地址不相等。(字符串常量池中的“abc”与s3,s4也不相等哦,因为内存地址不同)
例子3:字符串拼接
//当字符串池中有abc时,true
String s5 = "abc" + "def";//编译时就已经确定,这是从字符串池中返回的对象
String s6 = "abcdef";//存在于字符串池中的对象
String s7 = new String("abc") + new String("def");//运行时才生成,在堆里
System.out.println(s5 == s6);//true
System.out.println(s6 == s7);//false
同样一开始字符串为空,我们的s5是两个字面字符串的拼接,这里因为是字面值,所以编译期编译器就直接计算了拼接结果,然后放入了字符串池,所以这里就相当于String s5 = "abcdef"
。
而s7是两个String对象的拼接,在编译期编译器并不能识别他们拼接的结果,所以这里是在运行期才完成的拼接,这里先在堆中创建了“abc”对象,又在堆中创建了”def“对象,又因为String对象是不能更改的,所以是在堆中创建了一个新的字符串来返回给s7。
所以最终s5==s6,而s7!=s6。
总之一句话:字面字符串拼接编译期,而引用字符串拼接运行期。
例子4:String对象字符串拼接,jdk9和之前版本区别,StringBuilder
String s1 = "abc";
String s2 = "def";
String s3 = s1 + s2;
我们来看这样一段代码。针对这段代码,网上很多博客与教学视频都会如此解释:这里s3这行代码,其实是先实例化了一个StringBuilder对象,然后调用其append方法,分别将s1和s2的对象值拼接进来,然后继续链式调用tostring方法,返回一个字符串给s3,所以这里返回的对象实际是在堆内存中。
这种说法是没错的,我们看其字节码文件也能看出。
但实际上,这是在jdk9以前的jdk版本的情况。
如果我们使用的是jdk9的话,就不是这么个道理了。使用jdk9,不会再实例化StringBuilder对象,而是因为动态调用,返回一个String对象。当然这里还是在堆中。我们通过下面的字节码指令来证明。
这里用到了invokeDynamic指令。
例子5:intern方法
String s1 = new String("abc");
String s2 = s1.intern();
String s3 = "abc";
System.out.println(s2==s3);//true
String.intern()方法,是主动将该String对象放入字符串池中,如果字符串中已经存在,则不操作,并返回字符串池中的对象,如果不存在,则放入字符串池中,并返回字符串池中的对象。
这里注意无论怎样,返回的都是字符串池中的对象。
所以s1的对象是在堆内存里new的,而s2的是字符串池返回的,s3也是字符串池返回的,所以最后结果是true。
例子6:问创建了几个对象
String s1 = new String("abc");
问:这句代码创建了几个对象?
答:如果字符串池中没有“abc”,则创建了两个,字符串池中一个,堆中一个;如果字符串中有“abc”,则只创建了一个,就是堆中的哪个。
String s1 = "abc";
问:这句代码创建了几个对象?
答:如果字符串池中没有“abc”,则创建了一个,就是字符串池中那个;如果字符串中有“abc”,则没有创建。