引言
在JDK7之后,字符串常量池从Perm区移到了堆中,运行时常量池剩下的常量,如CONSTANT_class_info
,CONSTANT_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》