系统性学习请点击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常见问题分析

上面讲了字符串常量池,我们对于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,所以这里返回的对象实际是在堆内存中。

这种说法是没错的,我们看其字节码文件也能看出。

怎么查看Java 常量池 javastring常量池_bc


但实际上,这是在jdk9以前的jdk版本的情况。

如果我们使用的是jdk9的话,就不是这么个道理了。使用jdk9,不会再实例化StringBuilder对象,而是因为动态调用,返回一个String对象。当然这里还是在堆中。我们通过下面的字节码指令来证明。

怎么查看Java 常量池 javastring常量池_字符串_02


这里用到了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。
(2021.01.12 这里一定要注意是String对象,是将堆中的那个对象给返回,也就是常量池中存储的是堆中String对象的地址)

例子6:问创建了几个对象

String s1 = new String("abc");

问:这句代码创建了几个对象?
答:如果字符串池中没有“abc”,则创建了两个,字符串池中一个,堆中一个;如果字符串中有“abc”,则只创建了一个,就是堆中的哪个。

String s1 = "abc";

问:这句代码创建了几个对象?
答:如果字符串池中没有“abc”,则创建了一个,就是字符串池中那个;如果字符串中有“abc”,则没有创建。