字节码指令简介
Java虚拟机指令由操作码(Opcode)和跟随其后的零至多个操作数(Operand)组成。
-
操作码:一个字节长度的,代表某种特定操作含义的数字。
-
操作数:操作码需要的参数。
字节码与数据类型
opcode | byte | short | int | long | float | double | char | reference |
---|---|---|---|---|---|---|---|---|
Tstore | istore | lstore | fstore | dstore | astore | |||
Tinc | iinc | |||||||
Taload | baload | saload | iaload | laload | faload | daload | caload | aaload |
Tastore | bastore | sastore | iastore | lastore | fastore | dastore | castore | astore |
Tadd | iadd | ladd | fadd | dadd | ||||
Tsub | isub | lsub | fsub | dsub | ||||
Tmul | imul | lmul | fmul | dmul | ||||
Tdiv | idiv | ldiv | fdiv | ddiv | ||||
Trem | irem | lrem | frem | drem | ||||
Tneg | ineg | lneg | fneg | dneg | ||||
Tshl | ishl | lshl | ||||||
Tshr | ishr | lshr | ||||||
Tushr | iushr | lushr | ||||||
Tand | iand | land | ||||||
Tor | ior | lor | ||||||
Txor | ixor | lxor | ||||||
i2T | i2b | i2s | i2l | i2f | i2d | |||
l2T | l2i | l2f | l2d | |||||
f2T | f2i | f2l | f2d | |||||
d2T | d2i | d2l | d2f | |||||
Tcmp | lcmp | |||||||
Tcmpl | fcmpl | dcmpl | ||||||
Tcmpg | fcmpg | dcmpg | ||||||
if_TempOP | if_iempOP | if_aempOP | ||||||
Treturn | ireturn | lreturn | freturn | dreturn | areturn |
大部分指令都没有支持整数类型byte、 char和short, 没有任何指令支持boolean类型。 编译器会在编译期或运行期将byte和short类型的数据带符号扩展(Sign-Extend) 为相应的int类型数据, 将boolean和char类型数据零位扩展(Zero-Extend) 为相应的int类型数据。 与之类似, 在处理boolean、 byte、 short和char类型的数组时, 也会转换为使用对应的int类型的字节码指令来处理。 因此, 大多数对于boolean、 byte、 short和char类型数据的操作, 实际上都是使用相应的对int类型作为运算类型(Computational Type) 来进行的。
加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输 。
-
将一个局部变量加载到操作栈:
iload,iload_0,iload_1,iload_2,iload_3,lload,lload_0,lload_1,lload_2,lload_3,fload_0,fload_1,fload_2,fload_3,dload,dload_0,dload_1,dload_2,dload_3,aload,aload_0,aload_1,aload_2,aload_3
iload_0 表示将第0个变量槽中的int型变量推到操作数栈栈顶。
iload_1 表示将第1个变量槽中的int型变量推到操作数栈栈顶。
iload 后面会跟一个参数n,表示将第n个变量槽中的int行变量推送到操作数栈栈顶。
其余的指令同理。
-
将一个数值从操作数栈存储到局部变量表中:
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 #拓展本地变量的宽度,用于修改其他指令行为
public class Test2{public int inc(int a,int b,int c,int d,int e,int f,int h,int j,int k){int z = a + 9;return z;} }
利用
javac -g Test2.java
编译该源文件,再利用javap -v Test2.class
反编译:public int inc(int, int, int, int, int, int, int, int, int);descriptor: (IIIIIIIII)Iflags: ACC_PUBLICCode:stack=2, locals=11, args_size=100: iload_1 # 将第1个变量槽中的int型变量推送到栈顶,即将变量a推送到栈顶1: bipush 9 # 将int型常量9推送到栈顶3: iadd # 将栈顶的两个int型数值相加并压入栈顶,即a+94: istore 10 # 将栈顶int型数值存入指定的本地变量槽,及将a+9的结果赋值给变量Z6: iload 10 # 将第10个变量槽中的变量推送值栈顶8: ireturn # 弹栈,从当前方法返回int,及return z;LineNumberTable:line 3: 0line 4: 6LocalVariableTable:Start Length Slot Name Signature0 9 0 this LTest2;0 9 1 a I0 9 2 b I0 9 3 c I0 9 4 d I0 9 5 e I0 9 6 f I0 9 7 h I0 9 8 j I0 9 9 k I6 3 10 z I
运算指令
运算指令用于将操作数栈上的两个值进行某种特定的运算,并将结果重新压入操作栈顶。可分为对整型数据进行运算的指令和对浮点型数据进行运算指令。
不存在直接支持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
类型转换指令
类型转换指令可以将两种不同的数值类型相互转换 。
**宽化类型转换:**小范围类型向大范围类型的安全转换。
以下数值类型的宽化类型转换无序显示的转换指令:
1. int到long,float,double2. long到float,double3. float到double
窄化类型转换:窄化类型转换必须显示的使用转换指令来完成
i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f
窄化转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,很可能会出现精度丢失。
JVM将浮点值转换为int或者long时,需要遵循以下规则:
- 如果浮点值是NaN, 那转换结果就是int或long类型的0。
- 如果浮点值不是无穷大的话, 浮点值使用IEEE 754的向零舍入模式取整, 获得整数值v。 如果v在目标类型T(int或long) 的表示范围之类, 那转换结果就是v; 否则, 将根据v的符号, 转换为T所能表示的最大或者最小正数。
细节参看:IEEE 754
对象创建与访问指令
·创建类实例的指令: 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
操作数栈管理指令
·将操作数栈的栈顶一个或两个元素出栈: pop、 pop2
·复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶: dup、 dup2、 dup_x1、dup2_x1、 dup_x2、 dup2_x2
·将栈最顶端的两个数值互换: swap
控制转移指令
控制转移指令可以让Java虚拟机有条件或无条件地从指定位置指令(而不是控制转移指令) 的下一条指令继续执行程序, 从概念模型上理解, 可以认为控制指令就是在有条件或无条件地修改PC寄存器的值。
·条件分支: 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指令: 用于调用对象的实例方法, 根据对象的实际类型进行分派(虚方法分派) ,这也是Java语言中最常见的方法分派方式。
·invokeinterface指令: 用于调用接口方法, 它会在运行时搜索一个实现了这个接口方法的对象, 找出适合的方法进行调用。
·invokespecial指令: 用于调用一些需要特殊处理的实例方法, 包括实例初始化方法、 私有方法和父类方法。
·invokestatic指令: 用于调用类静态方法(static方法) 。
·invokedynamic指令: 用于在运行时动态解析出调用点限定符所引用的方法。 并执行该方法。 前面四条调用指令的分派逻辑都固化在Java虚拟机内部, 用户无法改变, 而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的
异常处理指令
athrow
处理异常是由异常表来完成的,而非字节码指令。
同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步, 这两种同步结构都是使用管程(Monitor, 更常见的是直接将它称为“锁”) 来实现的。
方法级的同步是隐式的, 无须通过字节码指令来控制, 它实现在方法调用和返回操作之中。 虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法。 当方法调用时, 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置, 如果设置了, 执行线程就要求先成功持有管程, 然后才能执行方法, 最后当方法完成(无论是正常完成还是非正常完成) 时释放管程。 在方法执行期间, 执行线程持有了管程, 其他任何线程都无法再获取到同一个管程。 如果一个同步方法执行期间抛出了异常, 并且在方法内部无法处理此异常, 那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的, Java虚拟机的指令集中monitorenter和monitorexit两条指令来支持synchronized关键字的语义, 正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持 .
下篇文章针对这点做实例分析。