熟悉并掌握字节码指令,懂得阅读字节码文件是Java程序员一项基本技能,本文就罗列一下基本的字节码指令。
加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输:
-
将一个局部变量加载到操作数栈: iload,iload_< n >,lload,lload_< n >,fload,fload_< n >,dload,dload_< n >,aload_< n >
-
将一个数值从操作数栈存储到局部变量表: istore,istore_< n >,lstore,lstore_< n >,fstore,fstore_< n >,dstore,dstore_< n >,astore,astore_< n >
-
将一个常量加载到操作数栈: bipush,sipush,ldc,ldc_w,ldc2,ldc2_w,aconst_null,iconst_m1,iconst_< i >,iconst_< l >,fconst_< f >,dconst_< d >
-
扩充局部变量表的访问索引指令: wide
运算指令
运算或算数指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入操作数栈顶。算数指令大体可分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令。没有直接支持byte,char,short和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,lshl,lushl
-
按位或指令: ior,lor
-
按位与指令: iand,land
-
按位异或指令: ixor,lxor
-
局部变量自增指令: iinc
-
比较指令: dcmpg,dcmpl,fcmpg,fcmpl,lcmp
类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,一般用于实现用户代码中显示类型转换操作。Java虚拟机的类型转换有如下两种:
-
宽化类型转换(小范围类型到大范围类型的转换,无需显示的转换指令)
- int类型到long,float或者double类型
- long类型到float,double类型
- float类型到double类型
-
窄化类型转换(大范围类型到小范围类型的非安全转化,可能导致精度损失,需要显示的转化指令来完成) : i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l,d2f
对象创建与访问指令
实例对象和数组都是对象,但是Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。下面是类实例和数组对象的创建与操作指令:
-
创建类实例的指令: new
-
创建数组的指令: newarray,anewarray,multinnewarray
-
访问类字段(static字段)和实例字段(实例变量): getfield,putfield,getstatic,putstatic
-
把一个数组元素加载到操作数栈的指令: bload,caload,saload,iaload,laload,faload,daload,aaload
-
将一个操作数栈的值存储到数组元素中的指令: bastore,castore,sastore,iastore,fastore,dastore,aastore
-
取数组长度的指令: arraylength
-
检查类实例类型的指令: instanceof,checkcast
操作数栈管理指令
-
操作数栈顶的一个或两个元素出栈: pop,pop2
-
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶: dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2
-
将栈最顶端的两个数值互换: swap
控制转移指令
控制转移指令如下:
-
条件分支: 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程序中显示抛出异常的操作都是由athrow指令来实现的。在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成。
同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都使用对象监视器(Monitor)来时实现。
方法级别的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法结构表中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否被声明为同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 是否被设置,如果被设置,执行线程就要先成功持有对象监视器,然后才能执行方法,当方法执行完成后释放对象监视器。在方法执行期间,执行线程拥有对象监视器,其他线程无法获取同一个对象监视器。如果一个同步方法在执行期间抛出了异常,并且在方法内部无法处理次异常,那么同步方法所持有的对象监视器将在异常被抛出同步方法之外时自动释放。
Java虚拟机指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义。编译器必须确保无论方法通过何种方式完成,方法调用过的每条 monitorenter 指令都必须执行其对应的 monitorexit 指令,无论这个方法是正常结束还是异常结束。
参考
《深入理解Java虚拟机》