字符串常量池

引言

在JDK7之后,字符串常量池从Perm区移到了堆中,运行时常量池剩下的常量,如CONSTANT_class_infoCONSTANT_Fieldref_info 等,还存放在Perm区。在JDK8中,HotSpot移除了Perm区用Metaspace(元空间)代替,此时,字符串常量池还是存放在堆中,运行时常量池放入了Metaspace中。

String的编译优化

如果两个final常量相加后进行赋值,那么在编译时,就会替换掉这个相加的过程,而改为直接赋值的操作。
如下代码:

    public static void main(String[] args) {
        final String str1 = "ab";
        final String str2 = "cd";

        String str3 = str1 + str2;
    }

经过反编译后

//Code
    Code:
      stack=1, locals=4, args_size=1
         0: ldc           #2                  // 将常量ab压入操作数栈 #2所对应的值是ab
         2: astore_1						  // 将操作数栈顶的值出栈,并存放到第1个局部变量表Solt中	
         3: ldc           #3                  // 将常量cd压入操作数栈 #3所对应的值是cd
         5: astore_2						  // 将操作数栈顶的值出栈,并存放到第2个局部变量表Solt中
         6: ldc           #4                  // 将常量abcd压入操作数栈 #4所对应的值是abcd
         8: astore_3						  // 将操作数栈顶的值出栈,并存放到第3个局部变量表Solt中		
         9: return

所以可以发现,字节码中直接对str3进行了赋值,没有进行相加的操作

String#intern

	String s = new String("abc");

该语句创建了2个对象,第一个对象是abc字符串存储在常量池中,第二个对象是在堆中的String对象。

	String s = "abc";

而这句话只创建了一个在字符串常量池的abc对象。
来看下面这段代码

public static void main(String[] args) {
    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

在JDK8的环境下,输出的是true。通过反编译,来看下运行的步骤

         0: new           #2                  // 为StringBuilder对象分配内存空间,并将地址压入操作数栈顶
         3: dup								  // 复制操作数栈顶值,并将其压入栈顶,也就是说此时操作数栈上有连续相同的两个对象地址
         4: invokespecial #3                  // 调用实例初始化方法<init>:()V,这个方法是一个实例方法,所以需要从操作数栈顶弹出一个对象引用,也就是说这一步会弹出一个之前入栈的对象地址
         7: new           #4                  // 为String对象分配内存空间,并将地址压入操作数栈顶
        10: dup								  // 复制操作数栈顶值,并将其压入栈顶
        11: ldc           #5                  // 将字符串1压入操作数栈顶
        13: invokespecial #6                  // 调用实例初始化方法(Ljava/lang/String;):()V,并弹出字符串1和String的对象地址
        16: invokevirtual #7                  // 调用实例初始化方法StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder,并弹出String对象地址和StringBuilder对象地址,执行完成后将StringBuilder对象压入栈顶
        19: new           #4                  // class java/lang/String
        22: dup
        23: ldc           #5                  // String 1
        25: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        28: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        31: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: astore_1						  //将操作数栈11String对象出栈,并存放到第一个局部变量Slot中,也就是s3中
        35: aload_1							  // 将局部变量表中的s3压入操作数栈顶
        36: invokevirtual #9                  // Method java/lang/String.intern:()Ljava/lang/String;
        39: pop								  // 将栈顶元素弹栈
        40: ldc           #10                 // String 11
        42: astore_2						  //将操作数栈顶"11"出栈,并存放到第2个局部变量Slot中,也就是s4中
	...
}

总结下上面的步骤

  • String s3 = new String("1") + new String("1"); 创建了5个对象,2个String对象,1个常量池中的"1"对象,1个StringBuiler对象,还有s3引用的对象,并且此时常量池中还没有"11"字符串常量。
  • s3.intern(); JDK8中字符串常量池不在Metaspace,转移到了堆中,所以字符串常量池中不需要再存储一份对象,可以直接存储堆中的引用。所以字符串常量池直接存的是s3的引用。
  • String s4 = "11"; 这句代码中"11"是显示声明的,因此会直接去字符串常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较 s3 == s4 是 true。而在JDK6中,返回的是false。

这也变相解释了字符串常量池从Metaspace转移到了Heap的作用,将字符串常量池中的常量直接引用堆内的对象,可以避免堆和字符串常量池都存在一份equals方法下相同的对象。

参考资料
https://blog.csdn.net/goldenfish1919/article/details/80410349
https://www.cnblogs.com/Kidezyq/p/8040338.html
《Java虚拟机规范  Java SE 8》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值