JVM–基础–22–字节码指令
1、字节码简介
- Java字节码由操作码和操作数组成。
- 操作码:1个字节长度,代表某种特定操作含义的数字
- 操作数:零至多个代表此操作码所需参数
2、字节码与数据类型
- 在字节码指令集中,大多数指令都有操作所对应的数据类型信息,比如iload表示从局部变量表中加载int型的数据到操作栈中.
- 除了long和double类型外,每个变量都占局部变量区中的一个变量槽(slot),而long及double会占用两个连续的变量槽。
- 大多数对于boolean、byte、short和char类型数据的操作,都使用相应的int类型作为运算类型。
- 每种数据类型和操作都有对应的指令,有一些指令可以在必要的时候将一些不被支持的数据类型转换为被支持的数据类型。
2.1、数据类型表
- 以数据类型为列,
- 以操作指令为行
- 其中为空的项即说明虚拟机不支持对这种数据类型进行这项操作。
3、加载和存储指令
3.1、指令
将一个局部变量加载到操作栈
iload、iload_<n>
lload、lload_<n>
fload、fload_<n>
dload、dload_<n>
aload、aload_<n>
将一个数值从操作数栈存储到局部变量表
istore、istore_<n>
lstore、lstore_<n>
fstore、fstore_<n>
dstore、dstore_<n>
astore、astore_<n>
将一个常量加载到操作数栈
bipush
sipush
ldc
ldc_w
ldc2_w
aconst_null
iconst_m1
iconst_<i>
lconst_<l>
fconst_<f>
dconst_<d>
扩充局部变量表的访问索引的指令:wide。
3.2、测试
public class Hello{
public int add(int a,int b)
{
int c=a+b;
int d=1;
int f=c+d;
return f;
}
}
3.3、解析
F:\>javap -v Hello.class
Classfile /F:/Hello.class
Last modified 2019-7-23; size 264 bytes
MD5 checksum d09a5fcab945489db588d5e862fad676
Compiled from "Hello.java"
public class Hello
SourceFile: "Hello.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // Hello
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 add
#9 = Utf8 (II)I
#10 = Utf8 SourceFile
#11 = Utf8 Hello.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 Hello
#14 = Utf8 java/lang/Object
{
public Hello();
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 1: 0
public int add(int, int);
flags: ACC_PUBLIC
Code:
stack=2, locals=6, args_size=3
0: iload_1 //将一个局部变量a加载到操作栈
1: iload_2 //将一个局部变量b加载到操作栈
2: iadd //a+b
3: istore_3 //将a+b的数值从操作数栈存储到局部变量表c中
4: iconst_1 //定义常量d=1
5: istore 4//将d的数值从操作数栈存储到局部变量表中d
7: iload_3 //将一个局部变量c加载到操作栈
8: iload 4 //将一个局部变量d加载到操作栈
10: iadd //c+d
11: istore 5 //将c+d的数值从操作数栈存储到局部变量表
13: iload 5 //将一个局部变量(c+d)加载到操作栈
15: ireturn //返回数值
LineNumberTable:
line 5: 0
line 6: 4
line 7: 7
line 8: 13
}
4、运算指令
用于对操作数栈上的值进行某种特定的运算。(这里不多说看上面的测试代码)
加法运算:iadd,ladd,fadd,dadd。
减法运算:isub,lsub,fsub,dsub。
乘法运算:imul,lmul,fmul,dmul。
除法运算:idiv,ldiv,fdiv,ddiv。
求余指令:irem,lrem,frem,drem。
取反指令:imeg,lmeg,fmeg,dmeg。
位移指令:ishl,ishr,iushr,lshl,lshr,lushr。
按位或指令:ior,lor。
按位与指令:iand,land。
按位异或指令:ixor,lxor。
局部变量自增指令:iinc。
比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp。
注:只有在除法指令(idiv,ldiv)和求余指令(irem,lrem)当出现除数为零时会导致虚拟机抛出AirtmeticException异常,其余整形和浮点型运算场景都不会抛出异常
5、类型转换指令
- 可以将两种不同数值类型进行相互转换。
- 虚拟机天然支持基本数据类型的宽化类型转换,例如int到long、flost、double等。
- 对于窄化数据类型转化则必须用显示的转换指令。
5.1、显示的转换指令
i2b(int -> boolean)
i2c(int -> char)
i2s(int -> short)
l2i(long -> int)
f2i(float -> int)
f2l(float -> long)
d2i(double -> int)
d2l(double -> long)
d2f(double -> float)
5.2、注意点
- int/long 类型窄化转换为整数类型T时,转换过程为丢弃除最低位N(T的数据类型长度)个字节以外的内容。
- 浮点值窄化转换为整数类型T(int/long)时
if(浮点值==NaN){
result = 0;
}else{
value = [浮点值]; //向下取整
if(T.min <= value <= T.max){ //value在T的表示范围内
result = value;
}else{
if(value > 0) result = T.max;
if(value < 0) result = T.min;
}
}
5.3、测试代码
public class Hello{
public long add()
{
int a=11;
long b=20;
long c=a+b;
return c;
}
}
5.4、解析
F:\>javap -v Hello.class
Classfile /F:/Hello.class
Last modified 2019-7-23; size 271 bytes
MD5 checksum dc4f5dbebf95b9a1357abb2a994824b8
Compiled from "Hello.java"
public class Hello
SourceFile: "Hello.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Long 20l
#4 = Class #15 // Hello
#5 = Class #16 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 add
#11 = Utf8 ()J
#12 = Utf8 SourceFile
#13 = Utf8 Hello.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Utf8 Hello
#16 = Utf8 java/lang/Object
{
public Hello();
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 1: 0
public long add();
flags: ACC_PUBLIC
Code:
stack=4, locals=6, args_size=1
0: bipush 11 //将一个常量a=11加载到操作数栈
2: istore_1 //将一个数值11从操作数栈存储到局部变量表
3: ldc2_w #2 // long 20l 一个常量b=20加载到操作数栈
6: lstore_2 //将一个数值b=20从操作数栈存储到局部变量表
7: iload_1 //将一个局部变量a=11加载到操作栈
8: i2l //操作栈 a=11从int 转换long
9: lload_2 //将一个局部变量b=20加载到操作栈
10: ladd //a+b
11: lstore 4 //将一个数值(a+b)从操作数栈存储到局部变量表
13: lload 4 //将一个局部变量(a+b)加载到操作栈
15: lreturn //返回
LineNumberTable:
line 5: 0
line 6: 3
line 7: 7
line 8: 13
}
6、对象创建与访问指令
6.1、指令
创建类实例的指令:
new
创建数组的指令:
newarray
anewarray
multianewarray
访问类字段(static字段)和实例字段(非static字段)的指令
getfield
putfield
getstatic
putstatic
将一个数组元素加载到操作数栈的指令:
baload
caload
saload
iaload
faload
daload
aaload
将一个操作数栈的值存储到数组元素中的指令
bastore
castore
iastore
sastore
fastore
fastore
dastore,aastore
取数组长度的指令:
arraylength
检查类实例类型的指令:
instanceof
checkcast
6.2、代码
public class Hello{
private int age;
public static void main(String[] args)
{
Hello hello=new Hello();
hello.setAge(11);
int c=hello.getAge();
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age=age;
}
}
6.3、解析
F:\>javap -v Hello.class
Classfile /F:/Hello.class
Last modified 2019-7-23; size 461 bytes
MD5 checksum 405d9ef6996eb40b92f9991ad03c76db
Compiled from "Hello.java"
public class Hello
SourceFile: "Hello.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#22 // java/lang/Object."<init>":()V
#2 = Class #23 // Hello
#3 = Methodref #2.#22 // Hello."<init>":()V
#4 = Methodref #2.#24 // Hello.setAge:(I)V
#5 = Methodref #2.#25 // Hello.getAge:()I
#6 = Fieldref #2.#26 // Hello.age:I
#7 = Class #27 // java/lang/Object
#8 = Utf8 age
#9 = Utf8 I
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 getAge
#17 = Utf8 ()I
#18 = Utf8 setAge
#19 = Utf8 (I)V
#20 = Utf8 SourceFile
#21 = Utf8 Hello.java
#22 = NameAndType #10:#11 // "<init>":()V
#23 = Utf8 Hello
#24 = NameAndType #18:#19 // setAge:(I)V
#25 = NameAndType #16:#17 // getAge:()I
#26 = NameAndType #8:#9 // age:I
#27 = Utf8 java/lang/Object
{
public Hello();
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 1: 0
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class Hello 创建类实例的指令
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: bipush 11
11: invokevirtual #4 // Method setAge:(I)V
14: aload_1
15: invokevirtual #5 // Method getAge:()I
18: istore_2
19: return
LineNumberTable:
line 8: 0
line 9: 8
line 10: 14
line 13: 19
public int getAge();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #6 // Field age:I 访问类字段(static字段)和实例字段(非static字段)的指令
4: ireturn
LineNumberTable:
line 16: 0
public void setAge(int);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #6 // Field age:I 访问类字段(static字段)和实例字段(非static字段)的指令
5: return
LineNumberTable:
line 19: 0
line 20: 5
}
7、操作数栈管理指令
将一个操作数栈的栈顶一个或两个元素出栈:
pop
pop2
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:
dup
dup2
dup_x1
dup2_x1
dup_x2
dup2_x2
将栈顶端的两个数值交换
swap
8、控制转移指令
可以让Java虚拟机有条件或者无条件的从指定的位置而不是控制转移指令的下一条指令继续执行程序。
8.1、指令
条件分支:
ifeq
ifit
ifle
ifgt
ifnull
ifnonnull
if_icmpeq
if_icmpne
if_icmplt
if_icmpgt
if_icmple
if_icmpge
if_acmpeq
if_acmpne
复合条件分支:
tableswitch
lookupswitch
无条件分支:
gosto
goto_w
jsr
jsr_w
ret
8.2、代码
public class Hello{
public static void main(String[] args)
{
int a=11;
int b=0;
if(a>0){
b=2;
}else{
b=1;
}
}
}
8.3、解析
F:\>javap -v Hello.class
Classfile /F:/Hello.class
Last modified 2019-7-23; size 321 bytes
MD5 checksum 344b92a34b16863c6264573b7620b263
Compiled from "Hello.java"
public class Hello
SourceFile: "Hello.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#13 // java/lang/Object."<init>":()V
#2 = Class #14 // Hello
#3 = Class #15 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 main
#9 = Utf8 ([Ljava/lang/String;)V
#10 = Utf8 StackMapTable
#11 = Utf8 SourceFile
#12 = Utf8 Hello.java
#13 = NameAndType #4:#5 // "<init>":()V
#14 = Utf8 Hello
#15 = Utf8 java/lang/Object
{
public Hello();
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 1: 0
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: bipush 11 //常量11加载到操作数栈
2: istore_1 //将一个数值11从操作数栈存储到局部变量表
3: iconst_0 //常量0加载到操作数栈
4: istore_2 //将一个数值0从操作数栈存储到局部变量表
5: iload_1 //将一个局部变量11加载到操作栈
6: ifle 14 //11 小于 ,跳转14,否则继续走下去
9: iconst_2 //常量2加载到操作数栈
10: istore_2 //将一个数值2从操作数栈存储到局部变量表
11: goto 16 //走到16行
14: iconst_1
15: istore_2
16: return //结束
LineNumberTable:
line 5: 0
line 6: 3
line 7: 5
line 8: 9
line 10: 14
line 12: 16
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 14
locals = [ int, int ]
frame_type = 1 /* same */
}
9、方法调用和返回指令
9.1、指令
invokevirtua
用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派)。
invokeinterface
用于调用接口方法,它在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
invokespecial
用于调用一些需要特殊处理的实例方法,包括实例的初始化方法,私有方法和父类方法。
invokestatic
用于调用类方法(static方法)
invokedynamic
用于运行时动态解析出调用点限定符所应用的方法,并执行该方法。(前面的分派逻辑都固化在虚拟机内部,而该指令的分派逻辑是由用户自定义)。
方法返回指令:
ireture(返回类型是int,short,byte,char,boolean时)
lreturn
freturn
dreturn
areturn
还有一条return供void方法、实例/类/接口的初始化方法使用
9.2、代码
public class Hello{
private int age;
public static void main(String[] args)
{
Hello hello=new Hello();
hello.setAge(11);
int c=hello.getAge();
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age=age;
}
}
9.3、解析
F:\>javap -v Hello.class
Classfile /F:/Hello.class
Last modified 2019-7-23; size 461 bytes
MD5 checksum 405d9ef6996eb40b92f9991ad03c76db
Compiled from "Hello.java"
public class Hello
SourceFile: "Hello.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#22 // java/lang/Object."<init>":()V
#2 = Class #23 // Hello
#3 = Methodref #2.#22 // Hello."<init>":()V
#4 = Methodref #2.#24 // Hello.setAge:(I)V
#5 = Methodref #2.#25 // Hello.getAge:()I
#6 = Fieldref #2.#26 // Hello.age:I
#7 = Class #27 // java/lang/Object
#8 = Utf8 age
#9 = Utf8 I
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 getAge
#17 = Utf8 ()I
#18 = Utf8 setAge
#19 = Utf8 (I)V
#20 = Utf8 SourceFile
#21 = Utf8 Hello.java
#22 = NameAndType #10:#11 // "<init>":()V
#23 = Utf8 Hello
#24 = NameAndType #18:#19 // setAge:(I)V
#25 = NameAndType #16:#17 // getAge:()I
#26 = NameAndType #8:#9 // age:I
#27 = Utf8 java/lang/Object
{
public Hello();
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 1: 0
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class Hello 创建类实例的指令
3: dup
4: invokespecial #3 // Method "<init>":()V 实例的初始化方法
7: astore_1
8: aload_1
9: bipush 11
11: invokevirtual #4 // Method setAge:(I)V 用于调用对象的实例方法
14: aload_1
15: invokevirtual #5 // Method getAge:()I 用于调用对象的实例方法
18: istore_2
19: return 、//方法返回指令
LineNumberTable:
line 8: 0
line 9: 8
line 10: 14
line 13: 19
public int getAge();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #6 // Field age:I 访问类字段(static字段)和实例字段(非static字段)的指令
4: ireturn
LineNumberTable:
line 16: 0
public void setAge(int);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #6 // Field age:I 访问类字段(static字段)和实例字段(非static字段)的指令
5: return
LineNumberTable:
line 19: 0
line 20: 5
}
10、异常处理指令
- 显式抛出异常指令:athrow
- 在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用throw语句显式抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。
10.1、测试
public class Hello{
public static void main(String[] args)
{
throw new RuntimeException("我是异常");
}
}
11、同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
11.1、方法级同步
- 方法级的同步是隐式的,即无须通过字节码指令来控制
- 它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
11.2、方法内部一段指令序列的同步
同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持。
11.3、测试