新旧博客已经迁移、维护在掘金平台,点这里 访问我的掘金博客主页
一、直接创建
1、图解创建过程
2、代码和字节码实战
2.1、常量池中创建字符串常量
执行过程:
直接创建字符串,压栈到字符串常量池,然后将字符串引用保存到本地变量池。
// Java代码
public class Test {
public static void main(String[] args) {
String a = "ab";
}
}
// 反编译出来的字节码文件
public class Test {
// 此处省去类的加载和调用main方法过程
public static void main(java.lang.String[]);
Code:
// 直接创建字符串对象,压栈到字符串常量池
0: ldc #2 // String ab
2: astore_1
3: return
}
2.2、堆中创建字符串对象
执行过程:
- 在堆中创建String对象;
- 常量池中声明字符串”ab“;
- 根据"ab"值调用String初始化方法初始化String对象
// Java代码
public class Test {
public static void main(String[] args) {
String a = new String("ab");
}
}
// 反编译出来的字节码文件
public class Test {
// 此处省去类的加载和调用main方法过程
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String ab
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: return
}
二、组合创建
在字符串拼接过程中,只要出现变量或者堆中字符串对象的拼接,会先行创建StringBuilder对象,使用该对象完成一个个字符串的拼接过程。
场景一:常量池中(字符串对象 + 字符串对象)
执行过程:
- 执行"a" + “b"的过程和创建"a”、"b"的过程一样,直接在字符串常量池中创建。
- 执行ab创建过程中,因为需要用到"a"、“b”,而此时字符串常量池中已经存在需要用到的"a"、“b”,不再重复创建,直接完成字符串的拼接,在常量池中创建字符串"ab",将地址赋值给字符串对象引用ab。
// Java代码
public class Test {
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "a" + "b";
}
}
// 反编译出来的字节码文件
public class Test {
// 此处省去类的加载和调用main方法过程
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
// 像上面字符串a和b的创建方式一样,直接在常量池中创建
6: ldc #4 // String ab
8: astore_3
9: return
}
场景二:常量池中(字符串对象引用 + 字符串对象引用)
执行过程:
- 字符串 a 和 b 的创建过程并不让人意外,直接在字符串常量池中声明。
- 在执行String ab = a + b;的时候做了什么呢?创建了StringBuilder对象,然后调用初始化方法初始化对象。
- 执行aload_1指令从本地变量中加载序号为1的变量也就是 字符串a的引用。
- 执行invokespecial指令调用StringBuilder内部append方法完成对字符串a的拼接。
- 重复3-4过程完成字符串b的拼接。
- 执行invokevirtual指令调用StringBuilder内部方法toString创建一个新的String对象。
- 将新String对象引用ab压栈。
// Java代码
public class Test {
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = a + b;
}
}
// 反编译出来的字节码文件
public class Test {
// 此处省去类的加载和调用main方法过程
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: return
}
场景三:常量池中字符串对象 + final常量
执行过程:
- 声明字符串"a"到常量池。
- 添加了final修饰词,为什么不再有字符串"b"的声明过程了呢?TODO 疑问待解决。
- 对于字符串ab直接在常量池中声明"ab"。
// Java代码
public class Test {
public static void main(String[] args) {
String a = "a";
final String b = "b";
String ab = "a" + b;
}
}
// 反编译出来的字节码文件
public class Test {
public Test();
// 此处省去类的加载和调用main方法过程
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String ab
5: astore_3
6: return
}
场景四:堆中对象 + 常量池中字符串对象引用
执行过程:
- 字符串常量池中创建字符串"b",然后保存字符串引用到本地变量。
- 创建StringBuilder对象,调用StringBuilder初始化方法。
- 在堆中创建String对象,在字符串常量池中声明字符串"a",然后初始化刚刚创建的String对象。
- 调用Stringbuilder对象的append方法拼接所创建的String对象的值。
- 再次调用StringBuilder对象的append方法拼接字符串b指向的"b"。
- 调用内部方法toString()生成String对象,保存该对象引用到本地变量池。
// Java代码
public class Test {
public static void main(String[] args) {
String b = "b";
String ab = new String("a") + b;
}
}
// 反编译出来的字节码文件
public class Test {
// 此处省去类的加载和调用main方法过程
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String b
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: new #5 // class java/lang/String
13: dup
14: ldc #6 // String a
16: invokespecial #7 // Method java/lang/String."<init>":(Ljava/lang/String;)V
19: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: aload_1
23: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
29: astore_2
30: return
}
三、捣乱的intern()函数
先看下面程序,如果一点不含糊地完全答对了,从珍爱生命的角度出发,就不用再继续看了。
闯关第一层
/**
* 直接声明字符串对象,调用intern()
*/
@Test
public void test1() {
String s1 = new String("hello world");
String s2 = s1.intern();
String s3 = "hello world";
System.out.println(s1 == s2);// false
System.out.println(s3 == s2);// true
}
闯关第二层
/**
* 拿此例子和不调用intern方法相比较
*/
@Test
public void test2() {
String s1 = new String("12") + new String("3");
String s2 = s1.intern();
String s3 = "123";
System.out.println(s1 == s2);// true
System.out.println(s2 == s3);// true
System.out.println(s1 == s3);// true
}
public void test2() {
String s1 = new String("12") + new String("3");
String s3 = "123";
System.out.println(s1 == s3);// false
}
闯关第三层
@Test
public void test3() {
String s1 = new String("12") + new String("3");
String s2 = s1.intern();
String s3 = new String("12") + new String("3");
String s4 = s3.intern();
String s5 = "123";
System.out.println(s1 == s2);// true
System.out.println(s2 == s4);// true
System.out.println(s5 == s3);// false
}
原理揭密
看图之前首先简单了解hotspot中两个数据结构:
Constant Pool:类似一个数组,存储常量字符串实体内容
String Table:底层结构类似HashTable,只是可不扩容,存储的是字符串的地址
补充
- 在字符串常量池中声明字符串后,我们可以认为hotspot就会默认调用intern()方法将地址保存在String Table中,实际上不是这样的,但是这个过程过于复杂,牵扯到加载和解析的细分,这里这么粗略的理解就可以。
思想抽象化之常量池技术
关于编译器加载和运行期加载
在将源代码编译为字节码的过程中,已经能确定的字符串资源存储在常量池中;而在编译器才能确认的字符串资源等等统统存放在堆中,不同类型的资源存放在不同的位置,就像现在住房里,厨房和书房、客厅、卧室的设计一般。
关于Constant Pool和String Table底层结构简单分析
Constant Pool中存放的是实际内容,String Table中存放的是引用,而这两个数据结构需要满足的要求之一其实都是快速查找,其中String Table更要满足快速查找功能,所以将地址引用设计成了散列码对应数据的结构,我们可以取出地址值,达到以最小的内存资源开销成本实现快速查找Constant Pool中资源的效果。当然这里还有很多疑问,比如如何设计String Table可以满足很久内容值快速查找定位,我们把堆中对象调用intern()的地址存放在String Table中做什么毕竟堆中的对象会被回收掉的,如果堆中对象被回收掉了,那么String Table中存储的地址是否也会被清除呢?这里面涉及的问题就更多了,你知道的越多你不知道的越多。。。
参考资料:
- https://www.zhihu.com/question/267818864?sort=created
- https://blog.csdn.net/Java_3y/article/details/104375866
- https://blog.csdn.net/kevindai007/article/details/56046570
- https://docs.oracle.com/javase/specs/jvms/se8/html/index.html