字节码理解Java String的两种创建方式
前言
通过该篇文章你将获得:
- 通过字节码理解Java String的两种创建方式
- 理解两种创建方式的不同。
- 加深理解Java的内存存储方式。
基础知识讲解
名词解释
- 字面量:诸如"abc"、"123"等,由两个引号引起来的部分叫做字面量,其在编译阶段就可确定。
- 常量池:存放常量的地方。
实例代码讲解
我们将通过以下代码和其字节码理解String的两种创建方式。
package com.pai;
public class StringInit {
public static void main(String[] args) {
String str1 = "abc";
String str2 = new String("def");
}
}
经过编译(javac StringInit.java
)与反编译(javap -v StringInit.class
),我们可以查看到如下的常量池:
Constant pool:
#1 = Methodref #7.#16 // java/lang/Object."<init>":()V
#2 = String #17 // abc
#3 = Class #18 // java/lang/String
#4 = String #19 // def
#5 = Methodref #3.#20 // java/lang/String."<init>":(Ljava/lang/String;)V
#6 = Class #21 // com/pai/StringInit
#7 = Class #22 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 SourceFile
#15 = Utf8 StringInit.java
#16 = NameAndType #8:#9 // "<init>":()V
#17 = Utf8 abc
#18 = Utf8 java/lang/String
#19 = Utf8 def
#20 = NameAndType #8:#23 // "<init>":(Ljava/lang/String;)V
#21 = Utf8 com/pai/StringInit
#22 = Utf8 java/lang/Object
#23 = Utf8 (Ljava/lang/String;)V
通过常量池我们可以总结如下几点:
- 两种创建方法的字面量都会被放到常量池中(
#2
和#4
) - 局部变量名不会被放到常量池中(
str1
和str2
)
经过编译(javac StringInit.java
)与反编译(javap -v StringInit.class
),我们可以查看到如下的指令:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // String abc
2: astore_1
3: new #3 // class java/lang/String
6: dup
7: ldc #4 // String def
9: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
13: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 13
接下来我们来详细讲解两种创建方式:
使用字面量创建
0: ldc #2 // String abc
2: astore_1
以下为指令讲解:
- 将常量池中
#2
字面量推送至栈顶。(压入栈顶) - 把栈顶的值存入1号本地变量
str1
,因此str1引用的是常量池地址。
使用对象创建
3: new #3 // class java/lang/String
6: dup
7: ldc #4 // String def
9: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
12: astore_2
以下为指令讲解:
- new指令在java堆上为String对象分配内存空间,并将地址压入操作数栈。
- dup指令复制操作数栈顶元素(复制String对象的地址,并压入栈中,此时栈顶有两个相同的地址)
- ldc指令把常量池中
#4
字面量推送至栈顶 - invokespecial指令调用实例初始化方法
String <init>(String)
,我们也看到该方法有一个参数,是String类型的,我们使用栈顶的字面量当入参数传入初始化方法。同时我们要知道是哪个个对象需要初始化,因此我们又从栈顶弹出一个引用地址(这也是为什么要dup复制一份的原因) - 将栈顶元素(对象地址)赋值给2号本地变量
str2
总结
我们可以看出字面量创建只有1个对象,就是常量池中的字面量,本地变量直接引用该地址,入栈1次。
使用对象创建使用了2个对象,常量池和堆,入栈3次。