字节码指令介绍

一. 字节码指令

Java虚拟机的指令由一个字节长度的、 代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成

1. 字节码与数据类型

在 Jvm 虚拟机指令中, 大多数的指令都是操作其对应的数据类型, 比如 iload 指令用于从局部变量表中加载 init 类型的数据到操作数栈中, fload 指令加载的是 float 类型的数据。 字节码指令有一定规则, 例如

  1. i: 代表 int 类型的的数据操作
  2. l: 代表 long 类型的的数据操作
  3. s: 代表 short 类型的的数据操作
  4. b: 代表 byte
  5. c: 代表 char
  6. f: 代表 float
  7. d: 代表 double
  8. a: 代表 reference

2. 加载和存储命令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容。

  1. 将一个局部变量加载到操作栈:iload iload_n>、 lload lload_n>、 fload 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_nulliconst_m1 iconst_i>、 lconst_l>、 fconst_f>、 dconst_d>。
  4. 扩充局部变量表的访问索引的指令:wide

3. 运算指令

运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。 大体上算术指令可以分为两种

  • 对整型数据进行运算的指令
  • 对浮点型数据进行运算的指令

无论是哪种算术指令,都使用Java虚拟机的数据类型,由于没有直接支持byte、short、 char和boolean类型的算术指令,对于这类数据的运算,应使用操作int类型的指令代替。 整数与浮点数的算术指令在溢出和被零除的时候也有各自不同的行为表现,所有的算术指令如下

  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 lneg 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

4. 类型转换指令

类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作

  1. int类型到long float或者double类型。
  2. long类型到float double类型。
  3. float类型到double类型。

5. 对象创建于访问指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。 对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素,这些指令如下

  1. 创建类实例的指令:new
  2. 创建数组的指令:newarray anewarray multianewarray
  3. 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield putfield getstatic putstatic
  4. 把一个数组元素加载到操作数栈的指令:baload caload saload iaload laloadfaload daload aaload
  5. 将一个操作数栈的值存储到数组元素中的指令:bastore castore sastore iastorefastore dastore aastore
  6. 取数组长度的指令:arraylength
  7. 检查类实例类型的指令:instanceof checkcast

6. 操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括

  1. 将操作数栈的栈顶一个或两个元素出栈:pop pop2
  2. 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup dup2dup_x1 dup2_x1 dup_x2 dup2_x2
  3. 将栈最顶端的两个数值互换:swap

7. 控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。 在Java虚拟机中有专门的指令集用来处理int和reference类型的条件分支比较操作,为了可以无须明显标识一个实体值是否null,也有专门的指令用来检测null值。

控制转移指令如下:

  1. 条件分支:ifeq iflt ifle ifne ifgt ifge ifnull ifnonnull if_icmpeq if_icmpneif_icmplt if_icmpgt if_icmple if_icmpge if_acmpeqif_acmpne
  2. 复合条件分支:tableswitch lookupswitch
  3. 无条件分支:goto goto_w jsr jsr_w ret

8. 方法调用和返回指令

方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、 byte、 char、 short和int类型时使用)、 lreturn、 freturn、 dreturn和areturn,另外还有一条return指令供声明为void的方法、 实例初始化方法以及类和接口的类初始化方法使用。

  1. invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
  2. invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
  3. invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、 私有方法和父类方法。
  4. invokestatic指令用于调用类方法(static方法)。
  5. invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的.

9. 异常处理指令

在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用throw语句显式抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。 例如,在前面介绍的整数运算中,当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。

而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和ret指令来实现,现在已经不用了),而是采用异常表来完成的。

10. 同步指令

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

方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。 虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。 当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。 在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。

同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持

11. 共有设计和私有实现

12. Class 文件结构的发展