基于Java SE 8虚拟机学习
1. Java字节码指令大全
参考官方规范文档 https://docs.oracle.com/javase/specs/index.html
Java虚拟机规范(Java SE 8版)
博客园大佬整理的Java字节码指令大全 https://www.cnblogs.com/longjee/p/8675771.html
2. 解析示例
字节码中每一步指令操作都与入栈出栈有关。
- 每个方法(含构造函数)都对应一个栈帧,每个栈帧都有一个操作数栈;
- 局部变量是一个数组,实例方法的栈帧中索引为0的那个元素通常为
this
,而静态方法的栈帧中则不是; - 调用实例方法前,会先将局部变量0(
this
)压入栈,若方法还接收其它参数,则依次将其它参数也压入栈然后传入(出栈)新的栈帧;
操作数栈:
- 后入先出(Last-In-First-Out)的列表为栈;
- 入栈即向栈中插入新元素;
- 最后入栈的元素处于栈顶;
- 最先入栈的元素在栈底;
- 出栈(弹出)则是取出最后入栈的元素;
2.1 示例一,以不同的方式初始化数值类型
该示例取自Numer系列代码解析中数值的“==”比较(未发布),描述基本类型与包装类型使用直接复制和new
关键字新建实例时,字节码指令的差异。
public class Main {
public static void main(String[] args) {
int i1 = 9;
int i2 = 9;
System.out.println("基本类型与基本类型比较,结果为" + (i1 == i2));
int i3 = 9;
Integer i4 = 9;
System.out.println("基本类型与包装类型比较,结果为" + (i3 == i4));
int i5 = 9;
Integer i6 = new Integer(9);
System.out.println("基本类型与包装类型比较(使用new关键字):" + (i5 == i6));
}
}
反编译:javap -c -v Main.class
第一部分常量池,常量池中包含方法引用Methodref
、字段引用Fieldref
、类Class
、字符串String
、字段或方法结构NameAndType
、字符常量值Utf8
等。
简单取第一个常量进行说明:
#1 常量池序号为1的常量,是方法引用Methodref
,其具体指向 #17 和 #44 两个常量,#1最终表现形式为java/lang/Object."<init>":()V
,此处指向是点分隔(#17.#44
);
- #17 常量池序号为17的常量,是类
Class
,其具体指向为 #61 这个常量,#17 最终表现形式为java/lang/Object
;- #61 常量池序号为61的常量,字符常量值
Utf8
,表现形式为java/lang/Object
;
- #61 常量池序号为61的常量,字符常量值
#61到#17的过程就相当于Java的类装载方法:
ClassLoader.getSystemClassLoader().loadClass("java.lang.Object");
- #44 常量池序号为44的常量,是方法结构
NameAndType
,其具体指向 #18 和 #19 两个常量,#44 最终表现形式为"<init>":()V
,此处指向是冒号分隔(#18:#19
);- #18 常量池序号为18的常量,字符常量值
Utf8
,表现形式为<init>
; - #19 常量池序号为19的常量,字符常量值
Utf8
,表现形式为()V
;
- #18 常量池序号为18的常量,字符常量值
下方示例中所有非字符常量最终都会指向一个字符常量值
Utf8
Classfile /C:/workspace/java_sdk/out/production/java_sdk/df/zhang/Main.class
Last modified 2019-5-12; size 1431 bytes
MD5 checksum bbb6c801e44a782a14d490c9103fe449
Compiled from "Main.java"
public class df.zhang.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #17.#44 // java/lang/Object."<init>":()V
#2 = Fieldref #45.#46 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #47 // java/lang/StringBuilder
#4 = Methodref #3.#44 // java/lang/StringBuilder."<init>":()V
#5 = String #48 // 基本类型与基本类型比较,结果为
#6 = Methodref #3.#49 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Methodref #3.#50 // java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
#8 = Methodref #3.#51 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Methodref #52.#53 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = Methodref #13.#54 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#11 = String #55 // 基本类型与包装类型比较,结果为
#12 = Methodref #13.#56 // java/lang/Integer.intValue:()I
#13 = Class #57 // java/lang/Integer
#14 = Methodref #13.#58 // java/lang/Integer."<init>":(I)V
#15 = String #59 // 基本类型与包装类型比较(使用new关键字):
#16 = Class #60 // df/zhang/Main
#17 = Class #61 // java/lang/Object
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Ldf/zhang/Main;
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 args
#28 = Utf8 [Ljava/lang/String;
#29 = Utf8 i1
#30 = Utf8 I
#31 = Utf8 i2
#32 = Utf8 i3
#33 = Utf8 i4
#34 = Utf8 Ljava/lang/Integer;
#35 = Utf8 i5
#36 = Utf8 i6
#37 = Utf8 StackMapTable
#38 = Class #28 // "[Ljava/lang/String;"
#39 = Class #62 // java/io/PrintStream
#40 = Class #47 // java/lang/StringBuilder
#41 = Class #57 // java/lang/Integer
#42 = Utf8 SourceFile
#43 = Utf8 Main.java
#44 = NameAndType #18:#19 // "<init>":()V
#45 = Class #63 // java/lang/System
#46 = NameAndType #64:#65 // out:Ljava/io/PrintStream;
#47 = Utf8 java/lang/StringBuilder
#48 = Utf8 基本类型与基本类型比较,结果为
#49 = NameAndType #66:#67 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#50 = NameAndType #66:#68 // append:(Z)Ljava/lang/StringBuilder;
#51 = NameAndType #69:#70 // toString:()Ljava/lang/String;
#52 = Class #62 // java/io/PrintStream
#53 = NameAndType #71:#72 // println:(Ljava/lang/String;)V
#54 = NameAndType #73:#74 // valueOf:(I)Ljava/lang/Integer;
#55 = Utf8 基本类型与包装类型比较,结果为
#56 = NameAndType #75:#76 // intValue:()I
#57 = Utf8 java/lang/Integer
#58 = NameAndType #18:#77 // "<init>":(I)V
#59 = Utf8 基本类型与包装类型比较(使用new关键字):
#60 = Utf8 df/zhang/Main
#61 = Utf8 java/lang/Object
#62 = Utf8 java/io/PrintStream
#63 = Utf8 java/lang/System
#64 = Utf8 out
#65 = Utf8 Ljava/io/PrintStream;
#66 = Utf8 append
#67 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#68 = Utf8 (Z)Ljava/lang/StringBuilder;
#69 = Utf8 toString
#70 = Utf8 ()Ljava/lang/String;
#71 = Utf8 println
#72 = Utf8 (Ljava/lang/String;)V
#73 = Utf8 valueOf
#74 = Utf8 (I)Ljava/lang/Integer;
#75 = Utf8 intValue
#76 = Utf8 ()I
#77 = Utf8 (I)V
$ javap -c Main.class
Compiled from "Main.java"
public class df.zhang.Main {
public df.zhang.Main();
Code:
0: aload_0
# 从局部变量0中加载对象引用this,入栈。上文描述类方法的栈帧中索引为0的那个元素通常为this
栈: {this}
1: invokespecial #1 // Method java/lang/Object."<init>":()V
# 调用初始化方法"<init>"初始化当前对象,将this传入新的栈帧。#1 对应Methodref // java/lang/Object."<init>":()V
栈: {}
4: return
# 此处是默认构造函数,可将构造函数理解为一个返回类型为void的方法
# "<init>"方法是编译器在编译时命名的初始化方法,无法通过编码方式实现。
先将this入栈是因需要将this传入"<init>"方法的栈帧。代码可描述为
# init(this); Java编程中,调用实例方法不需要传入this
以下解析中,部分被""包含的文本是为了"高亮"
public static void main(java.lang.String[]);
Code:
栈: {}
0: bipush 9
# [0],"byte"转换为"int"类型,并入栈。"byte"取值范围为:"-128 ~ 127",因此"9"实际上是"byte"类型
栈: {9}
2: istore_1
# [2],栈顶元素数字"9"出栈,并保存到局部变量1("LocalVariables[1]")中,即数值9保存到变量"int i1"中
栈: {}
i1 = 9;
3: bipush 9
# [3],"byte"转换为"int"类型,并入栈。
栈: {9}
5: istore_2
# [5],栈顶元素数字"9"出栈,并保存到局部变量2("LocalVariables[2]")中,即数值9保存到变量"int i2"中
栈: {}
i2 = 9;
6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
# [6],获取静态属性,"System.out"是类"System"中的静态常量,其类型为"java.io.PrintStream"
栈: {System.out}
9: new #3 // class java/lang/StringBuilder
# [9],新建"java.lang.StringBuilder"实例为"StringBuilder()",此时并未初始化该实例
# 可以发现代码中使用+号连接的字符串,实际上是使用"java.lang.StringBuilder"进行拼装的
栈: {StringBuilder(), System.out}
12: dup
# [12],复制一份未经初始化的"StringBuilder()"引用入栈,为"StringBuilder_DUP()"
# 栈中"StringBuilder_DUP()"和"StringBuilder()"指向同一个对象
栈: {StringBuilder_DUP(), StringBuilder(), System.out}
13: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
# [13],调用"<init>"方法初始化栈顶的"StringBuilder_DUP()","StringBuilder_DUP()"会被传入新的栈帧
# 因"<init>"无返回值,初始化结束后也不会有新的元素入栈
# 因此"init"("StringBuilder_DUP()"); 也就是初始化"StringBuilder()"
栈: {StringBuilder(), System.out}
16: ldc #5 // String 基本类型与基本类型比较,结果为
# [16],取得常量池中的字符串常量,并入栈。此处取出字符串常量池中("基本类型与基本类型比较,结果为")
栈: {"基本类型与基本类型比较,结果为", StringBuilder(), System.out}
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
# [18],调用"StringBuilder()"对象的实例方法"append()","基本类型与基本类型比较,结果为"和"StringBuilder()"会被传入新的栈帧
栈: {System.out}
# "StringBuilder().append()"方法执行结束后,会将"StringBuilder()"对象返回并入栈
栈: {StringBuilder(), System.out}
21: iload_1
# [21],从局部变量1("LocalVariables[1]")中加载"int"类型数值"9",入栈
栈: {9, StringBuilder(), System.out}
22: iload_2
# [22],从局部变量2("LocalVariables[2]")中加载"int"类型数值"9",入栈
栈: {9, 9, StringBuilder(), System.out}
23: if_icmpne 30
# [23],比较栈顶两个"int"类型的值,不相等就跳转到"[30]"
栈: {StringBuilder(), System.out}
26: iconst_1
# [26],"int"值1入栈,当值在-1 ~ 5范围内时会使用"iconst"指令,此处"1"值为"[23]"中的可能得到比较结果,代替"true"
栈: {1, StringBuilder(), System.out}
27: goto 31
# [27],跳转到位置31
30: iconst_0
# [30],"int"值0入栈,当值在-1 ~ 5范围内时会使用"iconst"指令,此处"0"值为"[23]"中的可能得到比较结果,代替"false"
栈: {0, StringBuilder(), System.out}
# "[26]"与"[30]"为流程控制,即等同于Java中的"if() {} else {}"
31: invokevirtual #7 // Method java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
由于代码运行结果中,(i1 == i2) = true,因此流程为[26, 27, 31]
栈: {1, StringBuilder(), System.out}
# [31],调用"StringBuilder()"对象的实例方法"append()","1"和"StringBuilder()"会被传入新的栈帧
栈: {System.out}
# "StringBuilder().append()"方法执行结束后,会将"StringBuilder()"对象返回并入栈
栈: {StringBuilder(), System.out}
34: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
# [34],调用"StringBuilder()"对象的实例方法"toString()"
栈: {System.out}
# "toString()"方法执行结束后,会将一个"String"对象返回并入栈,该字符串值为:"基本类型与基本类型比较,结果为true"
栈: {"基本类型与基本类型比较,结果为true", System.out}
37: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
# [37],调用"System.out"的方法"println()","基本类型与基本类型比较,结果为true"会被传入新的栈帧
"System.out.println()"方法无返回值,则方法执行结束后不会有任何新元素入栈
栈: {}
截止到以上,字节码中描述的代码如下:
int i1 = 9;
int i2 = 9;
System.out.println("基本类型与基本类型比较,结果为" + (i1 == i2));
# -------------------------------------------------------------------------------------------------------------------- #
栈: {}
40: bipush 9
# [40],"byte"转换为"int"类型,并入栈。"byte"取值范围为:"-128 ~ 127",因此"9"实际上是"byte"类型
栈: {9}
42: istore_3
# [42],栈顶元素数字"9"出栈,并保存到局部变量3("LocalVariables[3]")中,即数值9保存到变量"int i3"中
栈: {}
int i3 = 9;
43: bipush 9
# [43],"byte"转换为"int"类型,并入栈。
栈: {9}
45: invokestatic #10 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
# [45],调用"java.lang.Integer"的静态方法"valueOf()"新建"Integer()"实例,数值"9"会被传入新的栈帧
栈: {}
# "Integer.valueOf(9)"方法执行结束后,会将"Integer(9)"实例返回并入栈
栈: {Integer(9)}
48: astore 4
# [48],栈顶元素"Integer(9)"出栈,并保存到局部变量4("LocalVariables[4]")中,即实例"Integer(9)"保存到变量"Integer i4"中
栈: {}
Integer i4 = Integer(9);
50: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
# [50],获取静态属性,"System.out"是类"System"中的静态变量,其类型为"java.io.PrintStream"
栈: {System.out}
53: new #3 // class java/lang/StringBuilder
# [53],新建"java.lang.StringBuilder"实例为"StringBuilder()",此时并未初始化该实例
栈: {StringBuilder(), System.out}
56: dup
# [56],复制一份未经初始化的"StringBuilder()"引用入栈,为"StringBuilder_DUP()"
# 栈中"StringBuilder_DUP()"和"StringBuilder()"指向同一个对象。
栈: {StringBuilder_DUP(), StringBuilder(), System.out}
57: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
# [57],调用"<init>"方法初始化栈顶的"StringBuilder_DUP()","StringBuilder_DUP()"会被传入新的栈帧
# 因"<init>"无返回值,初始化结束后也不会有新的元素入栈。
# 因此"init"("StringBuilder_DUP()"); 也就是初始化"StringBuilder()"
栈: {StringBuilder(), System.out}
60: ldc #11 // String 基本类型与包装类型比较,结果为
# [60],取得常量池中的字符串常量,并入栈。此处取出字符串常量池中("基本类型与包装类型比较,结果为")
栈: {"基本类型与包装类型比较,结果为", StringBuilder(), System.out}
62: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
# [62],调用"StringBuilder()"对象的实例方法"append()","基本类型与包装类型比较,结果为"和"StringBuilder()"会被传入新的栈帧
栈: {System.out}
# "StringBuilder().append()"方法执行结束后,会将"StringBuilder()"对象返回并入栈
栈: {StringBuilder(), System.out}
65: iload_3
# [65],从局部变量3("LocalVariables[3]")中加载"int"类型数值"9",入栈
栈: {9, StringBuilder(), System.out}
66: aload 4
# [66],从局部变量4("LocalVariables[4]")中加载"Integer"类型引用"Integer(9)",入栈
栈: {Integer(9), 9, StringBuilder(), System.out}
68: invokevirtual #12 // Method java/lang/Integer.intValue:()I
# [68],调用"Integer(9)"的实例方法"intValue()","Integer(9)"会被传入到新的栈帧
栈: {9, StringBuilder(), System.out}
# "Integer(9).intValue()"方法执行结束后,会将数值"9"返回并入栈
栈: {9, 9, StringBuilder(), System.out}
71: if_icmpne 78
栈: {StringBuilder(), System.out}
74: iconst_1
栈: {1, StringBuilder(), System.out}
75: goto 79
78: iconst_0
栈: {0, StringBuilder(), System.out}
79: invokevirtual #7 // Method java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
82: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
85: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
栈: {}
截止到以上,字节码中描述的代码如下:
int i3 = 9;
Integer i4 = 9;
System.out.println("基本类型与包装类型比较,结果为" + (i3 == i4));
# -------------------------------------------------------------------------------------------------------------------- #
88: bipush 9
90: istore 5
栈: {}
92: new #13 // class java/lang/Integer
# [92],新建"java.lang.Integer"实例,此时并未初始化该实例
栈: {Integer(), System.out}
95: dup
# [95],复制未经初始化的"Integer()"引用入栈,,为"Integer_DUP()"
# 栈中"Integer_DUP()"和"Integer()"指向同一个对象。
栈: {Integer_DUP(), Integer(), System.out}
96: bipush 9
# [96],byte转换为int类型,并入栈。
栈: {9, Integer_DUP(), Integer(), System.out}
98: invokespecial #14 // Method java/lang/Integer."<init>":(I)V
# [98],调用"<init>"方法初始化栈顶的"Integer_DUP()","Integer_DUP()"会被传入新的栈帧,
# 因"<init>"无返回值,初始化结束后也不会有新的元素入栈。
# 因此"init"("Integer_DUP()"); 也就是初始化"Integer()"
栈: {Integer(9), System.out}
101: astore 6
# [101],栈顶元素引用"Integer(9)"出栈,并保存到局部变量6("LocalVariables[6]")中,即引用"Integer(9)"保存到变量"Integer i6"中
栈: {System.out}
Integer i6 = Integer(9);
103: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
106: new #3 // class java/lang/StringBuilder
109: dup
110: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
113: ldc #15 // String 基本类型与包装类型比较(使用new关键字):
115: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
118: iload 5
120: aload 6
122: invokevirtual #12 // Method java/lang/Integer.intValue:()I
125: if_icmpne 132
128: iconst_1
129: goto 133
132: iconst_0
133: invokevirtual #7 // Method java/lang/StringBuilder.append:(Z)Ljava/lang/StringBuilder;
136: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
139: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
栈: {}
截止到以上,字节码中描述的代码如下:
int i5 = 9;
Integer i6 = new Integer(9);
System.out.println("基本类型与包装类型比较(使用new关键字):" + (i5 == i6));
# -------------------------------------------------------------------------------------------------------------------- #
...
437: return
}
2.2 示例二,创建对象时与字节码指令dup的说明
关于dup
示例一中已经涉及到指令dup
,dup是单词duplicate(复制)的缩写,该指令复制栈顶一个字长的数据,然后将复制的数据压入栈顶。通过示例一可以发现,在包含dup
的指令集中,上下文都会是
new
dup
invokespecial # "<init>":()V
new
,新建某个类型的实例并压入栈顶,此时该实例尚未初始化;dup
,复制该未初始化的实例并压入栈顶,此时栈中有两个同类型未初始化的实例;invokespecial
,调用类的初始化方法,此时会讲栈顶的元素弹出并传入到初始化方法的栈帧中作为初始元素;- 初始化方法不会返回任何数据,因此最后栈顶仍为
new
时新建的那个实例,同时该实例已初始化并可以使用。
根据以上简略分析可以得出,
dup
指令复制的是实例的引用,而不是复制实例的整块内存。故而能使两个复制与被复制实例中任一实例被初始化后,另外一个实例可以正常使用。
引入一个简单的集合例子进行描述
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Hello World");
System.out.println(list);
}
}
$ javap -c Main.class
Compiled from "Main.java"
public class df.zhang.Main {
public df.zhang.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/util/ArrayList
# [0],新建集合实例"ArrayList()",并将其压入栈顶。该集合具体类型为"java.util.ArrayList"
栈:{ArrayList()}
3: dup
# [3],复制栈顶元素"ArrayList()"为"ArrayList_DUP()"
栈:{ArrayList_DUP(), ArrayList()}
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
# [4],调用将"java.util.ArrayList"的初始化方法"<init>",栈顶元素"ArrayList_DUP"会被传入到初始化方法的栈帧(每个方法都有对应的栈帧)中
由于dup是复制引用,因此初始化"ArrayList_DUP()"时,实际上也是初始化"ArrayList()"
栈:{ArrayList()}
7: astore_1
# [7],将"ArrayList()"弹出并保存到局部变量1中,即"List<String> list"
List<String> list = ArrayList();
栈:{}
8: aload_1
# [8],即将调用"ArrayList()"的接口方法"add()",因此需要将"ArrayList()"从局部变量1中取出并入栈
栈:{ArrayList()}
9: ldc #4 // String Hello World
# [9],从常量池取出字符串"Hello World",并入栈;
栈:{"Hello World", ArrayList()}
11: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
# [11],调用接口"java.util.List"的方法"add(Object objec)";
栈:{}
# "java.util.List.add(Object objec)"执行结束后会返回"boolean"类型值并入栈
# Java虚拟机中"boolean"类型值将会由"1"和"0"表示
16: pop
# [16],因不使用"add(Object objec)"的返回值,故"pop"弹出,将其舍弃
栈:{}
17: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
# [17],取得"System"中的静态常量"System.out"
栈: {System.out}
20: aload_1
# [20],再次加载局部变量1中的引用入栈
栈: {ArrayList(), System.out}
21: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
# [21],调用"java.io.PrintStream"的方法"println(Object object)",将"ArrayList()"和"System.out"传入新的栈帧中
栈: {}
"System.out.println(Object object)"无返回值
24: return
# [24],栈中无元素,无需"pop",直接返回"void"
}