字节码指令

Java虚拟机指令

Java虚拟机指令的构成

  • 一个字节长度
  • 代表着某种特定操作含义的数字(称为操作码,Opcode)
  • 跟随其后的零到多个代表此操作数所需的参数(称为操作数,Operands)
  • Java虚拟机采用的是面向操作数栈而不是寄存器的架构
    • 优点:放弃操作数长度对齐,可以省略很多填充和间隔符号。
    • 缺点:在某种程度上会导致解释执行字节码时损失一些性能。

      字节码与数据类型

  • 大多数指令都包含了其操作所对应的数据类型信息。
  • 大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的卫子夫来表名专门为哪种数据类型服务。
    • i代表对int类型的数据操作
    • l代表long
    • s代表short
    • b代表byte
    • c代表char
    • f代表float
    • d代表double
    • a代表reference
  • 也有一些指令的助记符中没有明确的指明操作类型的字母:
    • 如arraylength,它没有代表数据类型的特殊字符,但是操作数也只能是个数组类型的对象。
  • 还有一些指令,如goto则是与数据类型无关的。
  • 因此,Java虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它。也就是说,指令集将会故意被设计成非完全独立的。有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。

    加载和存储指令

    用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,包括:
  • 将一个局部变量加载到操作栈:iload、iload_、lload、lload_、fload、fload_、dload、dload_、aload、aload_
  • 将一个数值从操作数栈存储到局部变量表:istore、istore_、lstore、lstore_、fstore、fstore_、dstore、dstore_、astore、astore_
  • 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_、lconst_、fconst_、dconst_
  • 扩充局部变量表的访问索引的指令:wide。

    ==注: 等,表示一组指令,如iload_ 代表iload_0,iload_1,iload_2和iload3这几条指令。==

    运算指令

    用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入操作栈顶。指令如下:
  • 加法指令: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。

    ==注:byte、short、char和boolean类型没有直接支持的算数指令,对于这类数据的运算,应使用操作int类型的指令代替。==

    运算结果异常分析

  • 数据运算可能导致溢出,如两个很大的正整数相加,结果可能会是一个负数。Java虚拟机规范没有明确定义整型数据溢出的具体运算结果,仅规定了在处理整型数据时,只有除法指令以及求余指令中当出现除数为零时会导致虚拟机抛出ArithmeticException异常,其余任何整型数运算都不应该抛出运行时异常。
  • 浮点数运算时,所有运算结果必须舍入到适当的精度,非精确的结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的。
  • 浮点数转换 为整数时,Java虚拟机使用向零舍入模式,这种模式的舍入结果会导致数字被截断,所有小数部分的有效字节都会被丢弃。
  • 在处理浮点数运算时,不会抛出==任何运行时异常==,当一个操作产生溢出时,将会使用有符号的无穷大来表示,如果某个操作结果没有明确的数学定义的话,将会使用NaN来表示,所有使用NaN值作为操作数的算数操作,结果都会返回NaN。
  • 在对long类型数值进行比较时,虚拟机采用带符号的比较方式,而对浮点数值进行比较时,虚拟机会采用无信号比较*方式。

    类型转换指令

    可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作。
  • 直接支持宽化类型转换,及向上转,小转大。
    • int到long、float或者double类型。
    • long到float、double类型。
    • float到double类型。
  • 处理窄化类型转换时,必须显式地使用转化指令来完成。这些指令包括:
    • i2b
    • i2c
    • i2s
    • l2i
    • f2i
    • f2l
    • d2i
    • d2l
    • d2f
      但可能导致转换结果产生不同的正负号、不同的数量级,也可能导致数值的精度丢失。

      对象创建与访问指令

      虽然累实例和数组都是对象,但是Java虚拟机对类实例和数组的创建和操作使用了不同的字节码指令。指令如下:
  • 创建对象指令:new
  • 创建数组指令:newarray、anewarray、multianewarray
  • 访问类字段和实例字段的指令: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
  • 调用接口方法:invokeinsterface
  • 调用一些需要特殊处理的实例方法(实例初始化方法、私有方法和父类方法等):invokespecial
  • 调用静态方法(类方法):invokestatic
  • 在运行时动态解析出调用点限定符所引用的方法,并执行该方法:invokedynamic。

    异常处理指令

    显式抛出异常的操作(throw语句)都由athrow指令来实现。

    同步指令

    Java虚拟机可以支持方法及的同步和方法内部一段指令序列的同步,这两种同步结构都是使用Monitor来支持的。

    方法级的同步是隐式的,无需通过字节码指令来控制,它是现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声名为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED是否被设置,如果设置了,执行线程就要求先陈宫持有Monitor,然后才能执行这个方法,当方法完成时释放Monitor。