简介
JVM的
指令
由一个字节长度的、代表某种特定操作含义的数字以及跟随其后的零至多少代表此操作数所需的参数构成。
JVM采用的是面向操作数栈的架构,所以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。
字节码与数据类型
大部分与数据类型相关的字节码指令,它们的操作码中都有特殊字符来表明专门为那种类型服务:i代表int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。
下表列举了JVM所支持的与数据类型相关的字节码指令,若表中指令模板与数据类型坐标共同确定的格为空,则代表JVM不支持对这种数据类型执行这项操作。
加载与存储指令
加载与存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
- 将一个局部变量加载到操作栈:
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;
有一部分是以尖括号结尾的(例如iload_<n>),这些指令助记符实际上代表了一组指令(例如iload_<n>,它代表了iload_0、iload_1、iload_2和iload_3这几条指令) 。对于这几组特殊指令,它们省略掉了显式的操作数,实际上操作数就隐含在指令中。
运算指令
用于对两个操作数栈上的值进行某种特定运算, 并把结果重新存入到操作栈顶。
运算指令分为两种: 1. 对整型数据进行运算的指令; 2. 对浮点型数据进行运算的指令。
所有的算术指令:
- 加法指令: 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。
类型转换指令
将两种不同的数值类型相互转换。
数值类型转换分为两种:
- 宽化类型转换(Widening Numeric Conversion,小范围类型向大范围类型的安全转换):
(1)int类型到long、float、double类型;
(2)long类型到float、double类型;
(3)float类型到double类型。 - 窄化类型转换(Narrowing Numeric Conversion): 必须显式的使用转换指令来完成,这些转换指令包括i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f,窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,转换过程很可能会导致数值的精度丢失。
对象创建与访问指令
对类实例和数组的创建与操作使用了不同的字节码指令,对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素。
- 创建类实例的指令: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、checkcast
操作数栈管理指令
JVM提供了一些用于直接操作操作数栈的指令, 包括:
- 将操作数栈的栈顶一个或两个元素出栈: 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
方法调用和返回指令
方法调用指令分为五种:
- invokevirtual指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派);
- invokeinterface指令:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
- invokespecial指令: 用于调用一些需要特殊处理的实例方法, 包括实例初始化方法、私有方法和父类方法。
- invokestatic指令:用于调用类静态方法(static方法) 。
- invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法。并执行该方法。前面四条调用指令的分派逻辑都固化在Java虚拟机内部,用户无法改变, 而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
方法返回指令是根据返回值的类型区分的, 包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。
异常处理指令
在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用throw语句显式抛出异常的情况之外,《Java虚拟机规范》还规定了许多运行时异常会在其它JVM指令检测到异常状况时自动抛出。例如:整数运算中,当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。
JVM中,处理异常(catch语句)不是由字节码指令来实现的, 而是采用异常表来完成。
同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用
管程
(Monitor,更常见的是直接将它称为“锁”)来实现的。
方法级的同步是隐式的
,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。
同步一段指令集序列通常是由synchronized语句块来表示的
,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义, 正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持。
package demo;
public class Service {
void testMethod1(MyObject myObject) {
synchronized (myObject) {
myObject.printString();
}
}
}
void testMethod1(demo.MyObject);
descriptor: (Ldemo/MyObject;)V
flags:
Code:
stack=2, locals=4, args_size=2
0: aload_1 // 将对象myObject入栈
1: dup // 复制栈顶元素(myObject的引用)
2: astore_2 // 将栈顶元素存储到局部变量表变量 slow 2中
3: monitorenter // 以栈顶元素myObject作为锁,开始同步
4: aload_1 // 将局部变量slow 0的元素入栈
5: invokevirtual #2 // 调用printString()
8: aload_2 // 将局部变量slow 2的元素入栈
9: monitorexit // 退出同步
10: goto 18 // 正常结束则跳转至18行return
13: astore_3 // 异常流程开始 将栈顶元素存储到局部变量表变量slow 3中
14: aload_2 // 将局部变量slow 2的元素入栈
15: monitorexit // 退出同步
16: aload_3 // 将局部变量slow 3的元素入栈
17: athrow // 将异常对象抛出
18: return // 正常返回
Exception table:
from to target type
4 10 13 any
13 16 13 any
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须有其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束。
参考
《深入理Java虚拟机》周志明