字节码指令的概念

Java虚拟机的指令均是一个字节长度的,代表某种特定操作含义的操作码和操作数组成。Java虚拟机采用面向操作数栈而不是面向寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码,指令参数都放在操作数栈中。由于限制了Java虚拟机操作码的长度为一个字节(0~255),这意味着指令集的操作码总数不能够超过256条。同时因为编译后没用操作数长度对齐,导致虚拟机在处理超过一个字节的数据时需要在运行时从字节中重建出具体数据的结构。

常见的字节码指令

这些指令了解即可,无需记忆。

字节码和数据类型

JVM的指令集中大多数指令都包含其操作所对应的数据类型信息。

例子: iload指令用于从局部变量表中加载int型的数据到操作数栈中;fload则是加载float类型。

这两条指令的操作在虚拟机内部可能会由同一段代码来实现,但在class文件中它们必须拥有各自独立的操作码。

说明: i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。

image.png
image.png

注意: 大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的对int类型作为运算类型(Computational Type)来进行的。

加载和存储指令

  • 将一个局部变量加载到操作数栈:Tload
  • 将一个数值从操作数栈存储到局部变量表:Tstore
  • 将一个常数加载到操作数栈:bipush、sipush、ldc、ldcw、ldc2_w、aconst_null、iconst_m1、iconst、lconst、fconst、dconst_
  • 扩充局部变量表:wide

还有一些如访问对象的字段或数组元素的指令也会向操作数栈传输数据。带尖括号的指令,比如iload_,实际上表示了iload_0,iload_1,iload_2,iload_3这几条指令,它们省略掉了显式的操作数,不需要进行取操作数的动作。iload_0的语义和操作数为0时的iload指令完全一致

运算指令

  • 用于对两个操作数上的值进行某种特定运算,并把结果重新存入到操作栈顶。算数指令分两类:
  1. 对整型数据进行运算的指令
  2. 对浮点型数据进行运算的指令

    注意: 由于没有对byte、char、short和boolean类型的算数指令,应使用操作int类型的指令代替。

  • 进行浮点数计算时,所有的运算结果都必须舍入到适当精度,非精确的结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的。
  • 将浮点数转换为整数时,采用向零舍入模式,结果舍弃所有小数部分字节,获得一个最接近但是不大于原值的数字来作为最精确的舍入结果。

image.png

类型转换指令

Java虚拟机直接支持宽化类型转换,无需字节码指令
int -> long, float, double
long -> float, double
float -> double

解释: 宽化类型转化是小范围类型向大范围类型的安全转换

窄化类型转化需要转换指令:T2T

  • 窄化类型转换需要显示的使用指令完成:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f
  • 窄化类型转换可能会导致转换结果产生不同的正负号,不同的数量级的情况,常常导致精度丢失
  • 在将int或long类型窄化转换为整数类型T的时候,转换过程仅仅是简单丢弃除最低位N字节以外的内容,N是类型T的数据类型长度,这将可能导致转换结果与输入值有不同的正负号
  • JVM将浮点型窄化为整数型的时候,如果浮点数是NAN,那整形就是0,如果浮点数不是正无穷大,那就向零舍入取整,如果超过了整形的范围,就转换成整型所能表示的最接近的数
  • 窄化转换指令永远不可能导致虚拟机抛出运行时异常

    对象创建与访问指令

    Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令

  • 创建类:new

  • 创建数组:newarray,anewarray,multianewarray
  • 访问类字段和实例字段:getfield,putfield,getstatic,putstatic
  • 将数组加载到操作数栈:Taload
  • 将操作数栈的储存到数组元素中:Tastore
  • 取数组长度:arraylength
  • 检查类实力类型:instanceof,checkcast

    操作数栈管理指令

  • 出栈:pop,pop2

  • 复制栈顶的一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、duo_x1、dup2_x2、dup_x2
  • 将栈最顶端的两个数值互换:swap

    控制转移指令

    image.png
    在Java虚拟机中有专门的指令集用来处理int和reference类型的条件分支比较操作,为了可以无须明显标识一个数据的值是否null,也有专门的指令用来检测null值。cmp会先执行运算指令,然后将一个整型值返回到操作数栈中,随后再执行int型的条件分支比较来完成操作跳转

    方法调用和返回指令

  • invokevirtual指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派)

  • invokeinterface指令:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
  • invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
  • invokestatic指令:用于调用类静态方法(static方法)。
  • invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法。

有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。boolean,byte,char,short,int都是ireturn,其他还有lreturn,freturn,dreturn和areturn

异常处理指令

显示的抛出异常都由athrow指令实现,JVM中处理异常是由异常表来完成的

同步指令

  • JVM支持方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用管程来实现的
  • 方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中
  • 当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程
  • 同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持
  • 编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须有其对应的monitorexit指令

    注意: 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同 步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。