Java-字节码指令
字节码
是JVM的机器语言。JVM加载类文件时,对类中的每个方法,都会得到一个字节码流
。这些字节码流保存在JVM的方法区中。在程序运行过程中,当一个方法被调用时,它的字节码流就会被执行。
- 基础简介
- 方法的字节码流就是JVM的指令(instruction)序列。
Java虚拟机的指令
:由一个字节长度、代表着某种特定操作含义的数字(操作码Opcode
)以及跟随其后的0-n个代表此操作所需参数(操作数Operaands
)而构成。即指令=操作码+操作数,虚拟机中许多指令并不包含操作数,只有一个操作码。- JVM中,所有的计算都是围绕栈。因为JVM没有存储任意数值的寄存器,所有的操作数在计算开始之前,都必须先压入栈中。
源码如下:
1 | package com.xxo.demo.util; |
- 字节码和数据类型
在java虚拟机中,大多数的指令都包含了操作数所对应的数据类型
。这一点很重要,理解了这一点很多指令都能联想记忆,例如:iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload就是加载float类型的数据。下面列出:
- i–int
- l–long
- s–short
- b–byte
- c–char
- f–float
- d–double
- a–reference
加载和存储指令
每个操作如果需要从操作栈中读参数,则总是将这些参数出栈,如果操作有结果,总是会将结果入栈。
加载
:将数据从栈帧的局部变量表加载到操作数栈;存储
:将操作数栈存储到栈帧的局部变量表;
- 1、
将一个局部变量加载到操作栈
-load
。例如: iload、iload_n、lload、lload_n、fload、fload_n、dload、dload_n、aload、aload_n - 2、
将一个数值从操作数栈存储到局部变量表
-store
。例如: istore、istore_n、lstore、lstore_n、fstore、fstore_n、dstore、dstore_n、astore、astore_n - 3、
将一个常量加载到操作数栈
。例如: bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_i、lconst_l、fconst_f、dconst_d
例如,下面事例1:
1 | package com.xxo.demo.util; |
通过javap -c
编译后:
1 | public class com.xxo.demo.util.LoadStoreDemo extends java.lang.Object{ |
运算指令
算术指令
,用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。运算指令分为两种
:整型(int,long)、浮点型(float,double),对于没有直接支持 byte、short、char和boolean的算术指令使用int。
下面来看一下所有的算术指令:
加法指令
:iadd、ladd、fadd、dadd减法指令
:isub、lsub、fsub、dsub乘法指令
:imul、lmul、fmul、dmul除法指令
:idiv、ldiv、fdiv、ddiv求余指令
:irem、lrem、frem、drem取反指令
:ineg、lneg、fneg、dneg位移指令
:ishl、ishr、iushr、lshl、lshr、lushr按位或指令
:ior、lor按位与指令
:iand、land按位异或指令
:ixor、lxor局部变量自增指令
:iinc(是少见的直接更新一个局部变量而无需在操作数栈中进行读写的指令)比较指令
:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
java实例代码2:
1 | /** |
通过javap -c
编译后:
1 | public class com.xxo.demo.util.OperatorDemo extends java.lang.Object{ |
类型转换指令
类型转换指令
,数据类型进行相互转换,一般用于实现用户代码的显式类型转换操作,或者用来处理 Java 虚拟机字节码指令集中指令非完全独立
的问题。
宽化类型转换
- int 类型到 long、float 或者 double 类型
- long 类型到 float、double 类型
- float 类型到 double 类型
窄化类型转换
i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级,转换过程很可能会导致数值丢失精度。
1 | /** |
1 | public class com.xxo.demo.util.TypeCastDemo extends java.lang.Object{ |
对象创建与操作
虽然类实例和数组都是对象,但 Java 虚拟机对类实例和数组的创建与操作使用了不同的字节码指令:
创建类实例的指令:new
创建数组的指令:newarray
,anewarray
,multianewarray
访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者成为实例变量)的指令:getfield
、putfield
、getstatic
、putstatic
把一个数组元素加载到操作数栈的指令:baload
、caload
、saload
、iaload
、laload
、faload
、daload
、aaload
将一个操作数栈的值储存到数组元素中的指令:bastore
、castore
、sastore
、iastore
、fastore
、dastore
、aastore
取数组长度的指令:arraylength
检查类实例类型的指令:instanceof
、checkcas
看看下面的实例:
1 | /** |
编译后1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54public class com.xxo.demo.util.Instantiation extends java.lang.Object{
public static java.lang.String NAME;
public com.xxo.demo.util.Instantiation();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]); //main方法
Code:
0: iconst_5 //将常量5压入栈顶
1: newarray int //创建一个int类型的数组
3: astore_1 //将数组存储到局部变量表中,arr1中
4: aload_1 //将局部变量arr1加载到操作栈
5: iconst_0 //将常量0加载到操作数栈,
6: bipush 86 //将一个byte类型常量86加载到操作数栈
8: iastore //将一个操作数栈的值86储存到数组元素中,arr[0]中
9: aload_1 //将局部变量arr1加载到操作栈
10: iconst_2 //将常量2加载到操作数栈
11: bipush 19 //将一个byte类型常量19加载到操作数栈
13: iastore //将一个操作数栈的值19储存到数组元素中,arr[2]中
14: iconst_4 // 将常量4压入栈顶
15: anewarray #2; //class java/lang/String //创建一个String类型的数组
18: dup //直接操作操作数栈
19: iconst_0 //将常量0加载到操作数栈,
20: ldc #3; //String 2 //字符串常量加载到操作数栈,2
22: aastore //将引用类型的操作数栈的值储存到数组元素中,strs[0]中
23: dup //直接操作操作数栈
24: iconst_1 //将常量1加载到操作数栈,
25: ldc #4; //String xiao //字符串常量xiao加载到操作数栈
27: aastore //将引用类型的操作数栈的值储存到数组元素中,strs[1]中
28: dup //直接操作操作数栈
29: iconst_2 //将常量2加载到操作数栈,
30: ldc #5; //String mo //字符串常量加载到操作数栈
32: aastore //将引用类型的操作数栈的值储存到数组元素中,strs[2]中
33: dup //直接操作操作数栈
34: iconst_3 //将常量3加载到操作数栈,
35: getstatic #6; //Field NAME:Ljava/lang/String; //获取静态变量
38: aastore //将引用类型的操作数栈的值储存到变量中
39: astore_2 //将引用类型的操作数栈的值储存到变量中
40: new #2; //class java/lang/String //实例化一个对象
43: dup //直接操作操作数栈
44: ldc #7; //String abc //将字符串常量abc加载到操作数栈
46: invokespecial #8; //Method java/lang/String."<init>":(Ljava/lang/String;)V //调用实例方法new String
49: astore_3 //将引用类型的操作数栈的值储存到变量中,tc中
50: return //没有返回值
static {}; //静态区域
Code:
0: ldc #9; //String LIU BI //字符串常量‘LIU BI’加载到操作数栈
2: putstatic #6; //Field NAME:Ljava/lang/String;//实例静态字段
5: return //没有返回值
}
操作数栈管理指令
虚拟机提供了一些用于直接操作操作数栈的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2 和 swap。如上事例,直接操作数栈
控制转移指令
控制转移指令
,可以让JVM有条件或无条件地从指定指令转移到指令的下一条指令继续执行程序。控制转移指令包括有:
条件分支
:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt, if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne复合条件分支
:tableswitch、lookupswitch无条件分支
:goto、goto_w、jsr、jsr_w、ret
- 事例代码
1 | package com.xxo.demo.util; |
反编译后字节码如下:
1 | public class com.xxo.demo.util.ControlStatement extends java.lang.Object{ |
方法调用和返回指令
- 四条指令用于方法调用
invokevirtual
:指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。invokeinterface
:指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。invokespecial
:指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。invokestatic
:指令用于调用类方法(static方法)。
- 方法返回指令
ireturn
:(当返回值是 boolean、byte、char、short 和 int 类型时使用)lreturn
:返回long类型freturn
:返回floalt类型dreturn
:返回double类型areturn
:返回对象return
:指令表示为void的方法、实例初始化方法、类和接口的类初始化方法使用。
抛出异常
在程序中显式抛出异常的操作会由 athrow
指令实现,除了这种情况,还有别的异常会在其它 Java 虚拟机指令检测到异常状况时由虚拟机自动抛出。
同步
JVM可以支持方法级的同步
和方法内部一段指令序列的同步
,这两种同步结构都是使用管程(Monitor
)来支持的。
- 方法级的同步:是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。
虚拟机可以从方法常量池中的方法表结构(method_info Structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获得同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
- 同步一段指令集序列:通常是由Java语言中的synchronized块来表示的。
JVM的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现 synchronized 关键字需要编译器与 Java 虚拟机两者协作支持。
结构化锁定(Structured Locking)
:是指在方法调用期间每一个管程退出都与前面的管程进入相匹配的情形。因为无法保证所有提交给 Java 虚拟机执行的代码都满足结构化锁定,所以 Java 虚拟机允许(但不强制要求)通过以下两条规则来保证结构化锁定成立。假设 T 代表一条线程,M 代表一个管程的话:
T在方法执行时持有管程 M 的次数必须与 T 在方法完成(包括正常和非正常完成)时释放管程 M 的次数相等。
找方法调用过程中,任何时刻都不会出现线程 T 释放管程 M 的次数比 T 持有管程 M 次数多的情况。
请注意,在同步方法调用时自动持有和释放管程的过程也被认为是在方法调用期间发生。
1 | /** |
编译后,如下:
1 | public class com.xxo.demo.util.SynDemo extends java.lang.Object{ |