我们先来看Java代码
public class Test{
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
}
}
我们将它用javac指令
编译成Test.class
,再用javap -v
指令查看详细反编译后的字节码详细信息
Classfile /usr/local/xhf/mianshi/test_StringTable/Test.class
Last modified Sep 17, 2023; size 525 bytes
MD5 checksum 4c34e0eb00af28d5c7c5911c27398f5a
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #11.#20 // java/lang/Object."":()V
#2 = String #21 // a
#3 = String #22 // b
#4 = String #23 // ab
#5 = Class #24 // java/lang/StringBuilder
#6 = Methodref #5.#20 // java/lang/StringBuilder."":()V
#7 = Methodref #5.#25 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#26 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Methodref #27.#28 // java/lang/String.intern:()Ljava/lang/String;
#10 = Class #29 // Test
#11 = Class #30 // java/lang/Object
#12 = Utf8
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 SourceFile
#19 = Utf8 Test.java
#20 = NameAndType #12:#13 // "":()V
#21 = Utf8 a
#22 = Utf8 b
#23 = Utf8 ab
#24 = Utf8 java/lang/StringBuilder
#25 = NameAndType #31:#32 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#26 = NameAndType #33:#34 // toString:()Ljava/lang/String;
#27 = Class #35 // java/lang/String
#28 = NameAndType #36:#34 // intern:()Ljava/lang/String;
#29 = Utf8 Test
#30 = Utf8 java/lang/Object
#31 = Utf8 append
#32 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#33 = Utf8 toString
#34 = Utf8 ()Ljava/lang/String;
#35 = Utf8 java/lang/String
#36 = Utf8 intern
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=7, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: ldc #4 // String ab
31: astore 5
33: aload 4
35: invokevirtual #9 // Method java/lang/String.intern:()Ljava/lang/String;
38: astore 6
40: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 6
line 6: 9
line 7: 29
line 8: 33
line 9: 40
}
SourceFile: "Test.java"
你说太长了?没事,我们一点点看
Classfile
Classfile /usr/local/xhf/mianshi/test_StringTable/Test.class
Last modified Sep 17, 2023; size 525 bytes
MD5 checksum 4c34e0eb00af28d5c7c5911c27398f5a
Compiled from "Test.java"
Classfile信息:
- 类文件路径:/usr/local/xhf/mianshi/test_StringTable/Test.class
- 最后修改日期:Sep 17, 2023
- 文件大小:525字节
- MD5校验和:4c34e0eb00af28d5c7c5911c27398f5a
- 编译来源:Test.java
public class Test
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
类声明:
- 类的声明:public class Test
- 次版本号:0
- 主版本号:52
- 类标志(flags):ACC_PUBLIC, ACC_SUPER
Constant pool
Constant pool:
#...
常量池中的内容太多,现在只是大概介绍一下,详细信息解释后续字节码会有涉及
常量池:
- 常量池中包含了一系列的常量,包括方法引用、字符串、类名和字段等。常量>池中的#1到#36是各种常量的引用,用于在字节码中引用这些常量
public Test();
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0
public Test();
表示空参构造函数的声明
- descriptor: ()V
构造函数的描述符,()表示无参数,V表示返回类型为 void- flags: ACC_PUBLIC
访问标志,表示构造函数是公共的(ACC_PUBLIC),即可以从其他类中访问- Code:
表示字节码的开始,以下内容是字节码指令- stack=1, locals=1, args_size=1
字节码堆栈的信息。stack=1 表示操作数栈的深度为1,locals=1 表示本地变量表中有一个本地变量【this引用】,args_size=1 表示方法的参数数量为1【隐式的 this 参数】- 0: aload_0
将当前对象的引用加载到操作数栈上- 1: invokespecial #1
使用invokespecial指令,而#1则表示方法的符号引用。这个方法具体的含义是:Method java/lang/Object."
,也就是反编译的字节码中用//注释的那句话,这句话是程序添加上去的.":()V
- 现在让我们解释这句话的含义: java/lang/Object类的构造函数().这个构造函数没有参数,所以()中没有内容,:()V表示它返回void
- 那么,程序是如何判断这条指令的含义呢?答案是查询
constant pool
.
由#1
可知,该方法的符号引用是constant pool
中#1
所在位置
#1 = Methodref #11.#20 // java/lang/Object."
":()V - 继续查表
#11.#20
得到如下信息
#11 = Class #30 // java/lang/Object
【一个类,具体指向的是Object类】
#30 = Utf8 java/lang/Object
【UTF-8编码的字符串常量,内容是 “java/lang/Object”】#20 = NameAndType #12:#13 // "
【表示方法的名称和类型,名称是":()V ,表示构造函数;类型是:()V,表示无参数无返回值】
#12 = Utf8
【一个UTF-8编码的字符串常量,指向字符串""】 #13 = Utf8 ()V
【一个UTF-8编码的字符串常量,指向字符串"()V"】- 将上述查表得到的所有细腻些汇总,即可得到
#1 = Methodref
的含义,也就是//后面的解释内容- 4: return
表示方法的结束,4 是指令的位置或偏移量,表示这是方法字节码的第四行指令(从0开始计数)- LineNumberTable:
将字节码指令与源代码中的行号进行映射
- line 1: 0
(line 1)对应于源代码中的第 0 行(0)。这意味着构造函数的第一条字节码指令 0: aload_0 在源代码的第一行
public static void main(java.lang.String[]);
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=7, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: ldc #4 // String ab
31: astore 5
33: aload 4
35: invokevirtual #9 // Method java/lang/String.intern:()Ljava/lang/String;
38: astore 6
40: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 6
line 6: 9
line 7: 29
line 8: 33
line 9: 40
}
- stack=2, locals=7, args_size=1
- 栈的深度为2,第一个栈用于存放public Test()构造函数,第二个栈用于存放main方法;
- locals表示本地变量表中存在7个变量【main方法中new的变量:s1~s6 + String args[]】
- args_size=1表示方法参数列表中存在一个变量【String args[]】
- 0: ldc #2 // String a
- 0表示这是字节码的第一行指令
- ldc 表示load constant,加载常量到操作数栈上
#2
表示在constant pool
中查询的位置,查询得到的结果为
#2 = String #21 // a
#21 = Utf8 a
综合起来为String a,即将String a加载到栈中- 2: astore_1
- 将操作数栈上的引用值a存储到局部变量表中的第一个本地变量槽位。
- 3: ldc #3 // String b
- 将String b 加载到栈中
- 5: astore_2
- 将b存储到局部变量表中的第二个本地变量槽位
- 6: ldc #4 // String ab
- 将String ab加载到栈
- 8: astore_3
- 将String ab加载到本地变量
- 9: new #5 // class java/lang/StringBuilder
#5 = Class #24 // java/lang/StringBuilder
#24 = Utf8 java/lang/StringBuilder
在堆上创建一个新的 java.lang.StringBuilder 对象的实例,并将其引用推入操作数栈上- 12: dup
- 复制栈顶元素并将副本压入操作数栈上
- 13: invokespecial #6 // Method java/lang/StringBuilder.“
”: ()V
- 调用StringBuilder的构造方法
也就是new StringBuilder()
- 16: aload_1
- 将局部变量表中第一个本地变量槽位的值加载到操作数栈上,也就是
String a
- 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
- virtual 表示这是一次虚拟方法调用,也就是调用一个非静态方法
- invokevirtual 是指令的助记符,表示调用一个虚拟方法
- #7 表示常量池中的索引号,即方法的符号引用在常量池中的位置。具体的查询过程不在赘述,结果为
java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
也就是.appen(a)
- 20: aload_2
- 加载
String b
到操作数栈- 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
.append(b)
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;.toString()
,将9-21部指令结合在一起,得到new StringBuilder().append("a").append("b").toString();
- 27: astore 4
- 将操作数栈上的值存储到局部变量表中的第四个本地变量槽位,也就是
ab
- 29: ldc #4 // String ab
- 将ab加载到操作数栈中
- 31: astore 5
- 存储字符串到局部变量表的第五个本地变量 5 中
- 33: aload 4
- 加载局部变量表中的第四个本地变量 4(先前创建的字符串 “ab”)到操作数栈上。
- 35: invokevirtual #9 // Method java/lang/String.intern:()Ljava/lang/String;
- 调用字符串的 intern 方法,将其放入字符串常量池中。
- 38: astore 6
- 存储在字符串常量池中的字符串引用到局部变量表的第六个本地变量 6 中。
指令辨析
- ldc 指令:
ldc 是 “load constant” 的缩写,它用于将常量加载到操作数栈上。- aload 指令:
aload 是 “load reference” 的缩写,它用于加载引用类型的值(通常是对象引用)到操作数栈上- astore 指令:
astore 是 "store reference"的缩写,它用于将数据从栈上保存到方法的局部变量中