字节码指令

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

由于JVM采用面向操作数栈而不是寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码。

由于限制了操作码只有一个字节,所以操作码总数不会超过256个;又由于Class 文件格式放弃了编译后代码的操作数长度对齐,所以虚拟机在处理超过一个字节的数据时,需要在运行时从字节中重建出具体的数据结构(字节拼接),这会导致执行字节码时损失一些性能;放弃操作数长度对齐的好处是可以省略很多填充和间隔符号,用一个字节代表操作数码,可以获得短小精干的编译代码,这种追求小数据量、高传输率的设计是由Java语言设计之初面向网络、智能家电的技术背景决定的。

字节码与数据类型

在 Java 虚拟机的指令集中,大多数指令都包括了其其操作所对应的数据类型信息,例如,iload 用于从局部变量表中加载int类型的数据到操作数栈中。但是由于操作码最多只能有256个,因此JVM 的指令集对于特定的操作只提供了有限的类型相关指令。

大多数指令都没支持byte、char、short,没有任何指令支持boolean。编译器会在编译期或运行期将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将 boolean 和 char 类型零位扩展为相应的int类型数据,在处理这些类型的数组时,也会转换为使用对应的int类型的字节码指令处理。因此,大多数对于boolean、byte、short和char类型的操作,实际上都是使用响应的int类型作为运算类型。

字节码指令类型

加载和存储指令

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

  • 将局部变量加载到操作数栈:iload/iload_<n>、fload/fload_<n>、dload/dload_<n>、aload/aload_<n>

  • 将一个数值从操作数栈存储到局部变量表:istore/istore_<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

以尖括号结尾的是带有操作数的通用指令的特殊形式,它们把操作数包含在了指令中,不需要进行取操作数的动作。

运算指令

运算指令用于对两个操作数栈上的值进行特定运算,并把结果存入操作数栈顶。对于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

类型转换指令

用于将两种不同的数值类型相互转换,一般用于实现代码中的显示类型转换或者处理字节码指令集中数据类型相关指令无法与数据类型一一对应的情况。

Java 虚拟机直接支持下面的的宽化类型转换(Widening Numeric Conversions):

  • int ->long/float/double

  • long->float/double

  • float->double

处理窄化类型转换(Narrowing Numeric Conversions)时,必须显示的使用类型转换指令完成,转换指令包括:i2b/i2c/i2s/l2i/f2i/f2l/d2i/d2l/d2f。

窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级情况,转换过程很可能导致数值的精度丢失。

对象创建与访问指令

JVM 对类实例和数组的创建采用了不同的字节码指令:

  • 创建类实例:new

  • 创建数组指令:newarray/anewarray/multianewarray

  • 访问类字段和实例字段:getfield/putfield/getstatic/putstatic

  • 把一个数组元素加载到操作数栈:baload/caload/saload/iaload/laload/faload/daload/aaload

  • 把操作数栈的值存储到数组元素中:bastore/castore/sastore/iastore/lastore/fastore/dastore/astore

  • 取数组长度指令: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_cmpge/if_acmpeq/if_acmpne

  • 符合条件分支:tableswitch/lookupswitch

  • 无条件分支:goto/goto_w/jsr/jsr_w/ret

对于 boolean/byte/char/short 类型的条件分支比较操作,都是用int类型的比较指令完成;对于long/float/double类型的条件分支比较操作,则会先执行对应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后在执行int类型的条件分支比较操作来完成整个分支跳转。

方法调用和返回指令

  • invokevirtual:调用对象的实例方法,根据实例的类型进行分派(多态)

  • invokeinterface:调用接口的方法,会在运行时搜索一个实现了这个接口方法的对象,执行对应的方法

  • invokespecial:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法

  • invokestatic:调用静态方法

  • invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。invokedynamic 的分派逻辑是由用户设定的引导方法决定的。

  • 方法返回指令:ireturn/lreturn/freturn/dreturn/areturn

异常处理指令

显示抛出异常操作使用athrow指令实现,处理异常(catch语句)采用异常表完成。

同步指令

JVM 支持对方法和语句序列的同步,通过管程(monitor)完成。

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

如果同步方法执行期间发生了异常且方法内部没有处理,则管程将在异常抛出到方法之外时释放。

同步指令序列通过 synchronized 关键字完成,JVM 提供了 monitorenter 和 monitorexit两条指令来支持 synchronized 的语义。

例如下面代码:

void doWork(){
  synchronized(lock){
      doSomething();
}
}

void doSomething(){}
}

编译后的字节码如下:

void doWork();
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2                  // Field lock:Ljava/lang/Object;
         3: dup
         4: astore_1
         5: monitorenter
         6: aload_0
         7: invokevirtual #3                  // Method doSomething:()V
        10: aload_1
        11: monitorexit
        12: goto          20
        15: astore_2
        16: aload_1
        17: monitorexit
        18: aload_2
        19: athrow
        20: return

编译器必须确保无论方法是正常结束还是异常结束,调用的monitorenter的指令必须执行对应的monitorexit指令。

最后更新于

这有帮助吗?