JVM 字节码指令

2年前 (2022) 程序员胖胖胖虎阿
252 0 0

本文部分摘自《深入理解 Java 虚拟机》

简介

Java 虚拟机的指令由操作码 + 操作数组成,其中操作码是代表某种特定操作含义的数字,长度为一个字节,而操作数就是此操作所需的一个或多个参数。由于 Java 虚拟机采用面向操作数栈而非寄存器的架构,所以大多数指令都不包括操作数,只有一个操作码

既然限制了 JVM 操作码的长度为一个字节(0 ~ 255),也意味着指令集的操作码总数不超过 256 条。Class 文件格式放弃了编译后代码的操作数长度对齐,因此虚拟机在处理那些超过一个字节的数据时,不得不在运行时从字节中重建出具体数据的结构,这会损失一些性能,但也省略了大量的填充和间隔符号,尽可能得到短小精悍的编译代码

字节码和数据类型

在 Java 虚拟机的指令集中,大多数指令都包含其操作所对应的数据类型信息,每种数据类型都有特殊的字符来表示。但 Java 虚拟机的操作码长度只有一个字节,如果为每一种与数据类型相关的指令都支持 Java 虚拟机所有运行时数据类型的话,那指令的数量恐怕就会超过一字节所能表示的数量范围了

因此,Java 虚拟机对于特定的操作只提供了有限的类型相关指令去支持它,即并非每种数据类型和每一种操作都有对应的指令。下表就是特定操作与其支持数据类型的关系图,指令中的 T 可以替换为对应的数据类型,空格表示不支持这种数据类型执行这项操作

opcode byte short int long float double char reference
Tipush bipush sipush
Tconst iconst lconst fconst dconst aconst
Tload iload lload fload dload aload
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 aastore
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_TcmpOP if_icmpOP if_acmpOP
Treturn ireturn lreturn freturn dreturn areturn

可以发现,大部分指令都没有支持 byte、char、short、boolean,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展为相应的 int 类型数据,然后使用对应 int 类型的字节码指令来处理。因此,大多数对于 boolean、byte、short 和 char 类型数据的操作,实际上都是转换成 int 类型再进行操作

加载和存储指令

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

  • 将一个局部变量加载到操作数栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
  • 将一个数值从操作数栈存储到局部变量表: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_ml、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
  • 扩充局部变量表的访问索引的指令:wide

上面所列举的指令助记符中,有一部分是以尖括号结尾,如 iload_<n>,实际上代表了 iload_0、iload_1、iload_2 和 iload_3 这几条指令。iload_0 等价于 iload 0,同理,iload_1 等价与 iload 1 ……,它们省略了显示的操作数,不需要进行取操作数的动作,除此之外,它们的语义和原生的通用指令是完全一致

运算指令

算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶。所有的算术指令包括:

  • 加法指令: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

类型转换指令

类型转换指令可以将两种不同的数值类型相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用于开篇所提到的字节码指令集中数据类型相关指令与数据类型一一对应的问题

Java 支持小范围类型向大范围类型的安全转换,例如 int 到 long、float、double,与之相反的就必须显式地使用转换指令完成,这些指令包括 i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f 转换过程可能会导致数值的精度丢失

对象创建与访问指令

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

  • 创建类实例指令: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

操作数栈管理指令

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

  • 将操作数栈的栈顶一个或两个元素出栈:pop、pop2
  • 复制栈顶一个或两个数组并将复制值或双值的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
  • 将栈最顶端的两个数值互换:swap

控制转移指令

控制转移指令可以让 Java 虚拟机有条件或无条件地从指定位置指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或无条件地修改 PC 寄存器的值:

  • 条件分支:ifeq、iflt、ifle、ifne、ifgt、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 指令:用于调用类静态方法
  • invokedynamic 指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行

异常处理指令

在 Java 程序中显式地抛出异常的操作(throw)都由 athrow 指令来实现,除了用 throw 语句显式抛出异常的情况外,Java虚拟机规范还规定了许多运行时异常会在其他 Java 虚拟机指令检测到异常状况时自动抛出。对于处理异常(catch)操作,不是由字节码指令来实现,而是采用异常表

同步指令

Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都使用管程(Monitor,更常见的是直接称它为锁)来实现

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

同步一段指令集序列通常是由 Java 语言中的 synchronized 语句块来表示的,Java 虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义,两条指令之间包裹需要同步的指令序列,以实现同步效果

版权声明:程序员胖胖胖虎阿 发表于 2022年11月13日 下午8:08。
转载请注明:JVM 字节码指令 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...