Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令。每一个Java字节码指令是一个byte数字,并且有一个对应的助记符。
Java虚拟机常用指令
常量入栈指令
常量入栈指令的功能是将常数压入操作数栈,根据数据类型和入栈内容的不同,又可以分为const系列、push系列和ldc指令。
const系列:
aconst_null 将null压入操作数栈
iconst_m1 将-1压入操作数栈
iconst_x 将x压入栈
lconst_0 将长整数0压入栈
lconst_1 将长整数1压入栈
fconst_0 将浮点数0压入栈
dconst_0 将double型0压入栈
其中i表示整数,l表示长整数,f表示浮点数,d表示双精度浮点,a表示对象引用,一般用来压入数组的索引。
push系列:
bipush 接收8位整数作为参数
sipush 接收16位整数作为参数
ldc指令:
ldc 它可以接收一个8位的参数,该参数指向常量池中的int、float或者String的索引,将制定的内容压入堆栈。
ldc_w 它接收两个8位参数,能支持的索引范围大于ldc.
ldc2_w 压入的元素为long或者double类型
假设我们写一个这样的类
public class Calc {
public double calc() {
int a = 500000;
int b = 200;
long c = 50;
return (a + b) / c;
}
}
编译后,期编译的Calc.class文件位于/Users/admin/Downloads/calculate/target/classes/com/guanjian/calculate/test下面,进入该文件夹,执行命令
javap -v Calc
获得如下内容
警告: 二进制文件Calc包含com.guanjian.calculate.test.Calc
Classfile /Users/admin/Downloads/calculate/target/classes/com/guanjian/calculate/test/Calc.class
Last modified 2020-5-25; size 457 bytes
MD5 checksum 7088989dfee01d7aa7dcde0b23f91621
Compiled from "Calc.java"
public class com.guanjian.calculate.test.Calc
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#23 // java/lang/Object."<init>":()V
#2 = Integer 500000
#3 = Long 50l
#5 = Class #24 // com/guanjian/calculate/test/Calc
#6 = Class #25 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/guanjian/calculate/test/Calc;
#14 = Utf8 calc
#15 = Utf8 ()D
#16 = Utf8 a
#17 = Utf8 I
#18 = Utf8 b
#19 = Utf8 c
#20 = Utf8 J
#21 = Utf8 SourceFile
#22 = Utf8 Calc.java
#23 = NameAndType #7:#8 // "<init>":()V
#24 = Utf8 com/guanjian/calculate/test/Calc
#25 = Utf8 java/lang/Object
{
public com.guanjian.calculate.test.Calc();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/guanjian/calculate/test/Calc;
public double calc();
descriptor: ()D
flags: ACC_PUBLIC
Code:
stack=4, locals=5, args_size=1
0: ldc #2 // int 500000
2: istore_1
3: sipush 200
6: istore_2
7: ldc2_w #3 // long 50l
10: lstore_3
11: iload_1
12: iload_2
13: iadd
14: i2l
15: lload_3
16: ldiv
17: l2d
18: dreturn
LineNumberTable:
line 5: 0
line 6: 3
line 7: 7
line 8: 11
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 this Lcom/guanjian/calculate/test/Calc;
3 16 1 a I
7 12 2 b I
11 8 3 c J
}
SourceFile: "Calc.java"
其中Constant pool为常量池。其中有这么几行字节码指令
0: ldc #2 //常量池中#2为500000,将500000压入栈
2: istore_1
3: sipush 200。 //200转成二进制是11001000,大于8位小于16位,按16位压栈符压栈
6: istore_2
7: ldc2_w #3 //常量池中#3为长整数50l,故使用ldc2_w压入栈
局部变量压栈指令
局部变量压栈指令将给定的局部变量表中的数据压入操作数栈。
xload 通过指定参数的形式,将局部变量压入操作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个。
xload_n 表示将第n个局部变量压入操作数栈,比如iload_1、fload_0、aload_0,aload_n表示将一个对象引用压栈。(n为0到3)
xaload 表示将数组的元素压栈,比如saload、caload分别表示压入sort数组和char数组。指令xaload在执行时,要求操作数中栈顶元素为数组索引i,栈顶顺位第2个元素为数组a,该指令会弹出栈顶这两个元素,并将a[i]重新压入堆栈。
x取值 | 含义 |
---|---|
i | int整数 |
l | 长整数 |
f | 浮点数 |
d | 双精度浮点 |
a | 对象索引 |
b | byte |
c | char |
s | short |
现在我们来看java的这样一个方法
public void print(char[] cs,short[] s) {
System.out.println(s[0]);
System.out.println(cs[0]);
}
编译后的字节码为
public void print(char[], short[]);
descriptor: ([C[S)V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: getstatic #5
3: aload_2 //将数组引用s入栈,由于s是局部变量表第二个参数,故而尾数为2
4: iconst_0 //将索引0入栈
5: saload //弹出栈顶的两个元素(第一个是数组索引0,第二个是数组引用s),把short数组元素s[0]重新入栈
6: invokevirtual #6
9: getstatic #5
12: aload_1 //将数组引用cs入栈,由于cs是局部变量表第一个参数,故而尾数为1
13: iconst_0 //将索引0入栈
14: caload //弹出栈顶的两个元素(第一个是数组索引0,第二个是数组引用cs),把char数组元素cs[0]重新入栈
15: invokevirtual #7
18: return
LineNumberTable:
line 12: 0
line 13: 9
line 14: 18
LocalVariableTable: //局部变量表
Start Length Slot Name Signature
0 19 0 this Lcom/guanjian/calculate/test/Calc;
0 19 1 cs [C //局部变量表第一个参数
0 19 2 s [S。 //局部变量表第二个参数
MethodParameters:
Name Flags
cs
s
出栈装入局部变量表指令
出栈装入局部变量表指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的制定位置,用于给局部变量赋值。
xstore 通过指定参数的形式,将操作数栈中弹出一个值(x为i、l、f、d、a),当使用这个命令时,表示局部变量的数量可能超过了4个
xstore_n 将操作数栈中弹出一个值赋值给第n个局部变量(x为i、l、f、d、a)(n为0到3)
xastore 专门针对数组操作,用于给一个数组的给定索引赋值。
我们来看这样一段java代码
public void print(char[] cs,int[] s) {
int i,j,k,x;
x = 99;
s[0] = 77;
}
编译后字节码如下
public void print(char[], int[]);
descriptor: ([C[S)V
flags: ACC_PUBLIC
Code:
stack=3, locals=7, args_size=3
0: bipush 99 //由于99的二进制为1100011,不超过8位,故使用bipush压入栈
2: istore 6 //将栈顶元素99弹出栈,并赋值给局部变量表第6个变量x
4: aload_2 //将局部变量第2个s数组入栈
5: iconst_0 //将整数索引0入栈
6: bipush 77 //将77入栈,同上面的99
8: iastore //将77弹出栈,并赋值给局部变量表第2个数组s第0个索引
9: return
LineNumberTable:
line 13: 0
line 14: 4
line 15: 9
LocalVariableTable: //局部变量表
Start Length Slot Name Signature
0 10 0 this Lcom/guanjian/calculate/test/Calc;
0 10 1 cs [C //局部变量表第一个变量cs的char数组引用
0 10 2 s [I //局部变量表第二个变量s的int数组引用
4 6 6 x I //局部变量表第六个变量x为整数类型
MethodParameters:
Name Flags
cs
s
通用型操作
大部分数据操作指令是和数据类型相关的,但是无类型的指令还是有必要的,比如就栈操作而言,不是在所有时刻对栈的压入或者弹出都必须明确数据类型。通用型操作就提供了这种无需指明数据类型的操作。
nop 什么都不做
dup 将栈顶元素复制一份并再次压入栈顶,这样栈顶就有两份一模一样的元素了
pop 把一个元素从栈顶弹出,并且直接废弃
依然看Java代码
public void print(int i) {
Object obj = new Object();
obj.toString();
}
编译后的字节码如下
Constant pool:
#1 = Methodref #5.#30 // java/lang/Object."<init>":()V
#2 = Integer 500000
#3 = Long 50l
#5 = Class #31 // java/lang/Object
#6 = Methodref #5.#32 // java/lang/Object.toString:()Ljava/lang/String;
#7 = Class #33 // com/guanjian/calculate/test/Calc
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/guanjian/calculate/test/Calc;
#15 = Utf8 calc
#16 = Utf8 ()D
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 c
#21 = Utf8 J
#22 = Utf8 print
#23 = Utf8 (I)V
#24 = Utf8 i
#25 = Utf8 obj
#26 = Utf8 Ljava/lang/Object;
#27 = Utf8 MethodParameters
#28 = Utf8 SourceFile
#29 = Utf8 Calc.java
#30 = NameAndType #8:#9 // "<init>":()V
#31 = Utf8 java/lang/Object
#32 = NameAndType #34:#35 // toString:()Ljava/lang/String;
#33 = Utf8 com/guanjian/calculate/test/Calc
#34 = Utf8 toString
#35 = Utf8 ()Ljava/lang/String;
public void print(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=2
0: new #5 //new一个常量池#5的Object对象放入栈顶
3: dup //将object对象复制一份出来重新入栈
4: invokespecial #1 //调用Object类的构造函数
7: astore_2 //将栈顶的object对象弹出栈并赋值给局部变量表第二个变量obj
8: aload_2 //将局部变量表第二个变量obj入栈
9: invokevirtual #6 //执行常量池第#6的toString()方法
12: pop //将栈顶obj对象弹出栈废弃
13: return
LineNumberTable:
line 12: 0
line 13: 8
line 14: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 this Lcom/guanjian/calculate/test/Calc;
0 14 1 i I
8 6 2 obj Ljava/lang/Object;
注:pop指令只能丢弃1个字长(32位),如果要丢弃64位数据(long或者double),则需要使用pop2命令。如果要复制2个字长,则需要使用dup2指令。
类型转换指令
x2y 先将栈顶的x弹出,然后进行转换,转换后的y压入栈。x可能是i、f、l、d;y可能是i、f、l、d、c、s、b
java代码
public void print(int i) {
long l = i;
float f = l;
int j = (int) l;
}
编译后的字节码
public void print(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=6, args_size=2
0: iload_1 //将局部变量表第一个整型变量i压入栈
1: i2l //将栈顶i弹出转换成long类型压入栈
2: lstore_2 //将栈顶long类型变量弹出,赋值给局部变量表第二个long类型变量l
3: lload_2 //将局部变量表第二个long类型变量l压入栈
4: l2f //将栈顶l弹出转换成float类型压入栈
5: fstore 4 //将栈顶float类型变量弹出并赋值给局部变量表第4个变量f
7: lload_2 //将局部变量表第二个long类型变量l压入栈
8: l2i //将栈顶l弹出转换成int类型压入栈
9: istore 5 //将栈顶int类型变量弹出并赋值给局部变量表第5个整型变量j
11: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 7
line 15: 11
LocalVariableTable: //局部变量表
Start Length Slot Name Signature
0 12 0 this Lcom/guanjian/calculate/test/Calc;
0 12 1 i I
3 9 2 l J
7 5 4 f F
11 1 5 j I
在这些转换指令中只有i2b、i2c、i2s,但是没有b2i、c2i、s2i。也就是说,没有从byte、char或是short转换为其他数据类型的指令。来看一下如下代码
public void print(byte i) {
int k = i;
long l = i;
}
编译后的字节码
public void print(byte);
descriptor: (B)V
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=2
0: iload_1 //将局部变量表第一个byte类型变量i压入栈
1: istore_2 //将栈顶变量i弹出赋值给局部变量表第2个int类型变量k
2: iload_1 //将局部变量表第一个byte类型变量i压入栈
3: i2l //将栈顶变量i弹出转换成long类型压入栈,这里可以看到byte是当作int来识别的
4: lstore_3 //将栈顶long类型变量弹出并赋值给局部变量表第三个long类型变量l
5: return
LineNumberTable:
line 12: 0
line 13: 2
line 14: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/guanjian/calculate/test/Calc;
0 6 1 i B
2 4 2 k I
5 1 3 l J
我们可以看到byte类型转换成int类型,虚拟机并没有做实质性的转化处理,只是简单的通过操作数栈交换了两个数据。而将byte转换成long时,使用的是i2l,可以看到在内部byte在这里已经等同于int处理,类似的还有short。这种处理方式有两个特点
运算指令
运算指令为Java虚拟机提供了基本的加减乘除等运算功能,基本运行可以分为:加法、减法、乘法、除法、取余、数值取反、位运算、自增运算。
xadd 加法指令(x包括i、l、f、d)
xsub 减法指令
xmul 乘法指令
xdiv 除法指令
xrem 取余指令
xneg 数值取反
xinc 自增指令(x只能为i)
位运算指令
位移指令 ishl、ishr、iushr、lshl、lshr、lushr
xor 位或指令(x包括i、l)
xand 位与指令(x包括i、l)
xxor 位异或指令(第一个x包括i、l)
Java代码
public void print() {
float i = 8;
float j = -i;
i = -j;
int k = 33;
k += 3;
int l = k >> 1;
int t = ~l;
}
编译后的字节码
Constant pool: //常量池
#1 = Methodref #7.#31 // java/lang/Object."<init>":()V
#2 = Integer 500000
#3 = Long 50l
#5 = Float 8.0f
#6 = Class #32 // com/guanjian/calculate/test/Calc
#7 = Class #33 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/guanjian/calculate/test/Calc;
#15 = Utf8 calc
#16 = Utf8 ()D
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 c
#21 = Utf8 J
#22 = Utf8 print
#23 = Utf8 i
#24 = Utf8 F
#25 = Utf8 j
#26 = Utf8 k
#27 = Utf8 l
#28 = Utf8 t
#29 = Utf8 SourceFile
#30 = Utf8 Calc.java
#31 = NameAndType #8:#9 // "<init>":()V
#32 = Utf8 com/guanjian/calculate/test/Calc
#33 = Utf8 java/lang/Object
public void print();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=6, args_size=1
0: ldc #5 //将常量池#5的浮点数8.0f入栈
2: fstore_1 //将栈顶的8.0f弹出并赋值给局部变量表第一个浮点变量i
3: fload_1 //将局部变量表第一个浮点变量i入栈
4: fneg //将浮点变量i的数值取反
5: fstore_2 //将栈顶i取反后的值弹出并赋值给局部变量表第二个浮点变量j
6: fload_2 //将局部变量表第二个浮点变量j入栈
7: fneg //将浮点变量j的数值取反
8: fstore_1 //将栈顶j取反后的值弹出并赋值给局部变量表第一个浮点变量i
9: bipush 33 //将整数33压入栈
11: istore_3 //将栈顶的33弹出并赋值给局部变量表第三个整数变量k
12: iinc 3, 3 //将局部变量表第三个整数变量k自增3
15: iload_3 //将局部变量表第三个整数变量k入栈
16: iconst_1 //将常量1入栈
17: ishr //将栈中的两个元素都弹出,再把k做右位移1,再把结果k压入栈
18: istore 4 //将k右移后的结果从栈顶弹出并赋值给局部变量表第四个整型变量l
20: iload 4 //将局部变量表第四个整型变量l入栈
22: iconst_m1 //将常量-1入栈
23: ixor //将栈中的两个元素都弹出,将变量l与-1进行异或操作取反,再把结果l压入栈
24: istore 5 //将取反后的l从栈顶出栈并赋值给局部变量表第五个整型变量t
26: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 6
line 15: 9
line 16: 12
line 17: 15
line 18: 20
line 19: 26
LocalVariableTable: //局部变量表
Start Length Slot Name Signature
0 27 0 this Lcom/guanjian/calculate/test/Calc;
3 24 1 i F
6 21 2 j F
12 15 3 k I
20 7 4 l I
26 1 5 t I
注意:-1的二进制表示为全1的数字0xFF,任何数字与0xFF异或后,自然取反。
对象/数组操作指令
Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持。有一系列指令专门用于对象操作,可进一步细分为创建指令、字段访问指令、类型检查指令、数组操作指令。
创建指令
new 用于创建普通对象。它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。
newarray 用于创建基本类型的数组
anewarray 用于创建对象数组
multianewarray 用于创建多维数组