Java 虚拟机指令是由 (占用一个字节长度, 代表某种特定操作含义的数字) 操作码 Opcode, 以及跟随在其后的零至多个代表此操作所需参数的称为操作数 Operands 构成的. 由于 Java 虚拟机是面向操作数栈而不是寄存器的架构, 所以大多数指令都只有操作码, 而没有操作数.
字节码指令集是一种具有鲜明特点, 优劣势都很突出的指令集架构:
由于限定了 Java 虚拟机操作码的长度为 1 个字节, 指令集的操作码不能超过 256 条.
Class 文件格式放弃了编译后代码中操作数长度对齐, 这就意味者虚拟机处理那些超过一个字节数据的时候, 不得不在运行的时候从字节码中重建出具体数据的结构.
这种操作在一定程度上会降低一些性能, 但这样做的优势也非常的明显:
放弃了操作数长度对齐, 就意味着可以省略很多填充和间隔符号
用一个字节来表示操作码, 也是为了获取短小精悍的代码.
这种追求尽可能小数据量, 高传输效率的设计是由 Java 语言之初面向网络, 智能家电技术背景决定的.
Java 虚拟机解释器执行简单模型如下:
do{
计算 PC 寄存器的值 + 1;
根据 PC 寄存器只是位置, 从字节码流中取出操作码;
if(存在操作数) 从字节码中取出操作数;
执行操作码定义的操作;
}while(字节码长度 > 0);
字节码与数据类型
在 Java 虚拟机指令集中, 大多数的指令都包含了其操作所对应的数据类型信息. 例如, iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中.
但是由于虚拟机操作码长度只有一个字节, 所以包含了数据类型的操作码就为指令集的设计带来了很大的压力: 如果每一种数据类型相关的指令都支持 Java 虚拟机所有运行时数据类型的话, 那指令集的数据就会超过 256 个了. 因此虚拟机只提供了有限的指令集来支持所有的数据类型.
如 load 操作, 只有 iload,lload,fload,dload,aload 用来支持 int,long,float,double,reference 类型的入栈, 而对于 boolean ,byte,short 和 char 则没有专门的指令来进行运算. 编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展为 int 类型的数据, 将 boolean 和 char 类型的数据零位扩展为相应的 int 类型数据. 与之类似, 在处理 boolean,byte,short 和 char 类型的数组时, 也会发生转换. 因此, 大多数对于 boolean,byte,short 和 char 类型数据的擦操作, 实际上都是使用相应的 int 类型作为运算类型.
加载和存储指令
加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输.
1)将一个局部变量加载到操作数栈的指令包括: iload,iload_<n>,lload,lload_<n>,float, fload_<n>,dload,dload_<n>,aload,aload_<n>.
2)将一个数值从操作数栈存储到局部变量表的指令: 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_ml,iconst_<i>,lconst_<l>,fconst_<f>,dconst_<d>
4)局部变量表的访问索引指令: wide
一部分以尖括号结尾的指令代表了一组指令, 如 iload_<i>, 代表了 iload_0,iload_1 等, 这几组指令都是带有一个操作数的通用指令.
运算指令
算术指令用于对两个操作数栈上的值进行某种特定运算, 并把结果重新存入到操作栈顶.
1)加法指令: iadd,ladd,fadd,dadd
2)减法指令: isub,lsub,fsub,dsub
3)乘法指令: imul,lmul,fmul,dmul
4)除法指令: idiv,ldiv,fdiv,ddiv
5)求余指令: irem,lrem,frem,drem
6)取反指令: ineg,leng,fneg,dneg
7)位移指令: ishl,ishr,iushr,lshl,lshr,lushr
8)按位或指令: ior,lor
9)按位与指令: iand,land
10)按位异或指令: ixor,lxor
11)局部变量自增指令: iinc
12)比较指令: dcmpg,dcmpl,fcmpg,fcmpl,lcmp
Java 虚拟机没有明确规定整型数据溢出的情况, 但规定了处理整型数据时, 只有除法和求余指令出现除数为 0 时会导致虚拟机抛出异常.
Java 虚拟机要求在浮点数运算的时候, 所有结果否必须舍入到适当的精度, 如果有两种可表示的形式与该值一样, 会优先选择最低有效位为零的. 称之为最接近数舍入模式.
浮点数向整数转换的时候, Java 虚拟机使用 IEEE 754 标准中的向零舍入模式, 这种模式舍入的结果会导致数字被截断, 所有小数部分的有效字节会被丢掉.
类型转换指令
类型转换指令将两种 Java 虚拟机数值类型相互转换, 这些操作一般用于实现用户代码的显式类型转换操作.
JVM 直接就支持宽化类型转换(小范围类型向大范围类型转换):
1)int 类型到 long,float,double 类型
2)long 类型到 float,double 类型
3)float 到 double 类型
但在处理窄化类型转换时, 必须显式使用转换指令来完成, 这些指令包括: i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l 和 d2f.
将 int 或 long 窄化为整型 T 的时候, 仅仅简单的把除了低位的 N 个字节以外的内容丢弃, N 是 T 的长度. 这有可能导致转换结果与输入值有不同的正负号.
在将一个浮点值窄化为整数类型 T(仅限于 int 和 long 类型), 将遵循以下转换规则:
1)如果浮点值是 NaN , 呐转换结果就是 int 或 long 类型的 0
2)如果浮点值不是无穷大, 浮点值使用 IEEE 754 的向零舍入模式取整, 获得整数 v, 如果 v 在 T 表示范围之内, 那就过就是 v
3)否则, 根据 v 的符号, 转换为 T 所能表示的最大或者最小正数
对象创建与访问指令
虽然类实例和数组都是对象, Java 虚拟机对类实例和数组的创建与操作使用了不同的字节码指令.
1)创建实例的指令: new
2)创建数组的指令: newarray,anewarray,multianewarray
3)访问字段指令: getfield,putfield,getstatic,putstatic
4)把数组元素加载到操作数栈指令: baload,caload,saload,iaload,laload,faload,daload,aaload
5)将操作数栈的数值存储到数组元素中执行: bastore,castore,castore,sastore,iastore,fastore,dastore,aastore
6)取数组长度指令: arraylength JVM 支持方法级同步和方法内部一段指令序列同步, 这两种都是通过 moniter 实现的.
7)检查实例类型指令: instanceof,checkcast
操作数栈管理指令
如同操作一个普通数据结构中的堆栈那样, Java 虚拟机提供了一些用于直接操作操作数栈的指令, 包括:
1)将操作数栈的栈顶一个或两个元素出栈: pop,pop2
2)复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶: dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2.
3)将栈最顶端的两个数值互换: swap
控制转移指令
让 JVM 有条件或无条件从指定指令而不是控制转移指令的下一条指令继续执行程序. 控制转移指令包括:
1)条件分支: ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt 等
2)复合条件分支: tableswitch,lookupswitch
3)无条件分支: goto,goto_w,jsr,jsr_w,ret
JVM 中有专门的指令集处理 int 和 reference 类型的条件分支比较操作, 为了可以无明显标示一个实体值是否是 null, 有专门的指令检测 null 值. boolean 类型和 byte 类型, char 类型和 short 类型的条件分支比较操作, 都使用 int 类型的比较指令完成, 而 long,float,double 条件分支比较操作, 由相应类型的比较运算指令, 运算指令会返回一个整型值到操作数栈中, 随后再执行 int 类型的条件比较操作完成整个分支跳转. 各种类型的比较都最终会转化为 int 类型的比较操作.
方法调用和返回指令
invokevirtual 指令: 调用对象的实例方法, 根据对象的实际类型进行分派(虚拟机分派).
invokeinterface 指令: 调用接口方法, 在运行时搜索一个实现这个接口方法的对象, 找出合适的方法进行调用.
invokespecial: 调用需要特殊处理的实例方法, 包括实例初始化方法, 私有方法和父类方法
invokestatic: 调用类方法(static)
方法返回指令是根据返回值的类型区分的, 包括 ireturn(返回值是 boolean,byte,char,short 和 int),lreturn,freturn,drturn 和 areturn, 另外一个 return 供 void 方法, 实例初始化方法, 类和接口的类初始化 i 方法使用.
异常处理指令
在 Java 程序中显式抛出异常的操作 (throw 语句) 都有 athrow 指令来实现, 除了用 throw 语句显示抛出异常情况外, Java 虚拟机规范还规定了许多运行时异常会在其他 Java 虚拟机指令检测到异常状况时自动抛出.
在 Java 虚拟机中, 处理异常不是由字节码指令来实现的, 而是采用异常表来完成的.
同步指令
方法级的同步是隐式的, 无需通过字节码指令来控制, 它实现在方法调用和返回操作中. 虚拟机从方法常量池中的方法标结构中的 ACC_SYNCHRONIZED 标志区分是否是同步方法. 方法调用时, 调用指令会检查该标志是否被设置, 若设置, 执行线程持有 moniter, 然后执行方法, 最后完成方法时释放 moniter.
同步一段指令集序列, 通常由 synchronized 块标示, JVM 指令集中有 monitorenter 和 monitorexit 来支持 synchronized 语义.
结构化锁定是指方法调用期间每一个 monitor 退出都与前面 monitor 进入相匹配的情形. JVM 通过以下两条规则来保证结结构化锁成立(T 代表一线程, M 代表一个 monitor):
1)T 在方法执行时持有 M 的次数必须与 T 在方法完成时释放的 M 次数相等
2)任何时刻都不会出现 T 释放 M 的次数比 T 持有 M 的次数多的情况
---------------------
来源: http://www.bubuko.com/infodetail-2843274.html