字符串常量池

    技术2022-07-21  80

    字符串常量池

    字符串常量池存在于堆中,但是和new的对象不在一个地方,虽然都在堆中。字符串常量池在java8中最小应为1009,其底层原理是一个HashSet。因此每一个字面量在字符串常量池中只允许存在一份。所以上面的三个引用都指向同一个字符串,它们三个是==为true的。

    字符串变量拼接操作底层原理

    如上所示,当拼接的式子中只要出现了一个或者更多的字符串变量引用,则java的处理过程是:

    在堆中new一个StringBuilder对象;然后调用append()方法拼接;最后调用toString()方法得到一个String字符串对象。

    jvm栈中的局部变量表: 可以看到,局部变量表的第一个槽位是this,而不是s1,这个要注意一下。

    字符串常量或者引用常量的拼接

    字符串常量的拼接

    public void test1() { String s1 = "abc"; String s2 = "def"; String s3 = "abc" + "def"; String s4 = s1 + s2; String s5 = "abcdef"; System.out.println(s3 == s5); }

    上面是java的源代码,下面是经过编译后的class文件。可以看出来,当两个字符串常量拼接的时候,编译器会进行预处理,将其直接拼接好变成一个字符串常量。

    public void test1() { String s1 = "abc"; String s2 = "def"; String s3 = "abcdef"; (new StringBuilder()).append(s1).append(s2).toString(); String s5 = "abcdef"; System.out.println(s3 == s5); }

    字符串引用常量的拼接 如上所示,虽然s1和s2还是两个引用,但是这里是引用常量,在拼接的时候(s4)不是通过new一个StringBuilder进行拼接的。而是在编译器中进行了优化,图的右边是对应的class文件,可以看到编译器已经进行了预处理。

    intern()的使用

    查看文档: 总结起来就是当常量池中已经存在了与该字符串equals()的字符串常量,则直接将该常量的引用返回给调用者;否则就在常量池创建一个与该字符串equals()的字符串常量,同样将该常量的引用返回。 实例:

    public void test1() { final String s1 = "abc"; final String s2 = "def"; String s3 = "abc" + "def"; String s4 = s1 + s2; String s5 = "abcdef"; String s6 = "ab"; String s7 = "cd"; String s8 = s6 + s7 + "ef";//s8在堆中而不在常量池 //s8调用intern()方法,则会将常量池中的"abcdef"引用返回给s9 String s9 = s8.intern(); System.out.println(s9 == s5);//true System.out.println(s9 == s8);//false }

    创建了多少对象

    String string = new String("aaa") + new String("bbb");

    上面这条语句一共创建了多少对象?下面是对应的汇编语句,可以看到: 首先创建了一个StringBuilder对象用于拼接; 然后创建了一个String对象,使用常量池中字符串"aaa"进行初始化; 调用append()方法用于拼接; 然后又创建了一个String对象,使用常量池中字符串常量"bbb"初始化; 再次调用append()方法进行拼接; 然后调用toString()方法将StringBuilder对象(“aaabbb”)变为String对象(“aaabbb”),这里又需要创建一个新的对象,注意 这里并没有在常量池中生成"aaabbb"这个字符串常量,见下图的toString()方法指令。 综上所述,一共创建了4个对象,不包括常量池中的字符串,假如字符串常量池中已经存在"aaa"和"bbb"的话,如果之前并没有存在,那就是创建了6个对象。 下面是toString()方法的指令,可以看到里面并没有ldc指令,就只new了一个String对象。 补充一个知识点: ldc指令 该命令负责把数值常量或String常量值从常量池中推送至栈顶。该命令后面需要给一个表示常量在常量池中位置(编号)的参数。

    垃圾回收

    下面来看一个例子:

    String s[] = new String[1024*1024]; for (int i = 0; i < 1024*1024; i++) { //重复产生"0"-"9"字符串,并将常量池的引用回传 //这时在堆中new的字符串没有被引用,因此会被垃圾回收器回收 // s[i] = String.valueOf(i % 10).intern(); //valueOf(int)会调用Integer.toString(),而Integer.toString()反过来调用了String类的构造方法,产生一个新的String对象。这里是将新对象的引用给了s[i]。因为new出来的对象存在堆中,并且它有一个强引用,所以不会进行垃圾回收。这时在堆中就有一百多万个String对象。 s[i] = String.valueOf(i % 10); }

    上图是s[i] = String.valueOf(i % 10).intern();对应的内存情况,可以看到String[]的占用空间是4百多万字节,这是因为每一个s[i]变量在堆中都占用了4个字节,而这个空间是被局部变量表中的s所引用的,因此不会被回收。而String对象占用的只有300k字节,只有12k个实例,而不是1024k个实例,很显然这些对象被清理掉了。这是因为使用了intern()之后,返回的是常量池中的引用,常量池中只会存在1份"0"-"9"这10个字符串实例,而堆中的字符串实例则有太多太多值一样的重复行为,并且由于s[i]引用的的常量池中对象,堆中的这些对象失去了引用,因此被GC了。 下面我们看一下s[i] = String.valueOf(i % 10);对应的内存占用情况,如下图所示,String[]占用的和上面还是一样。而String对象有1062k个实例,和我们申请的差不多,说明这些创建的堆中对象没有被回收。 字符串数组的内存模型: 如上图所示,如果不使用intern(),则s[i]指向的是堆中new的字符串引用,堆中new了多少个就会存在多少个,即使这些字符串中有重复值。但是调用了intern()之后,会在字符串常量池中创建字符串对象,由于字符串常量池是一个HashMap,因此里面不允许有重复值出现。这时再将常量池中对象的引用给s[i],而之前在堆中new的字符串对象由于失去了引用,所以变得不可达,会被GC,从而可以达到节省大量空间的目的。因此这里启发我们,如果要大量创建重复的字符串对象时,我们可以使用intern()方法节省内存空间。 图是我用visio画的,这里复制过来可能里面的字看不是很清,但是结构是清楚的。

    Processed: 0.008, SQL: 9