Java虚拟机结构(第4部分)字节码指令集简介

2.11 字节码指令集简介

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

如果忽略异常处理,那Java虚拟机的解释器使用下面这个伪代码的循环即可有效地工作:

do { 
    自动计算PC寄存器以及从PC寄存器的位置取出操作码; 
    if (存在操作数) 
        取出操作数; 执行操作码所定义的操作 
} while (处理下一次循环);

操作数的数量以及长度取决于操作码

字节码指令流应当都是单字节对齐的,只有“tableswitch”和“lookupswitch”两条指令例外

限制Java虚拟机操作码的长度为一个字节,并且放弃了编译后代码的参数长度对齐

2.11.1 数据类型与Java虚拟机

在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息

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

编译器会在编译期或运行期会将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据

类型 long double 属于分类二 其他是分类一

有部分对操作栈进行操作的Java虚拟机指令(例如pop和swap指令)是与具体类型无关的,不过这些指令也必须受到运算类型分类的限制

2.11.2 加载和存储指令

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输

  • 将一个局部变量加载到操作栈的指令包括有:iloadiload_<n>lloadlload_<n>floadfload_<n>dloaddload_<n>aloadaload_<n>

  • 将一个数值从操作数栈存储到局部变量表的指令包括有:istoreistore_<n>lstorelstore_<n>fstorefstore_<n>dstoredstore_<n>astoreastore_<n>

  • 将一个常量加载到操作数栈的指令包括有:bipushsipushldcldc_wldc2_waconst_nulliconst_m1iconst_<i>lconst_<l>fconst_<f>dconst_<d>

有一部分是以尖括号结尾的(例如iload_<n>),这些指令助记符实际上是代表了一组指令(例如iload_<n>,它代表了iload_0iload_1iload_2iload_3这几条指令)这几组指令都是某个带有一个操作数的通用指令。还有些在尖括号之间的字母制定了指令隐含操作数的数据类型。

2.11.3 运算指令

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

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

规定了在处理整型数据时,只有除法指令(idiv和ldiv)以及求余指令(irem和lrem)出现除数为零时会导致虚拟机抛出异常,如果发生了这种情况,虚拟机将会抛出ArithmeitcException异常。

Java虚拟机在处理浮点数时完全支持IEEE 754中定义的非正规浮点数值(Denormalized Floating-Point Numbers,§2.3.2)和逐级下溢(Gradual Underflow)。

Java虚拟机要求在进行浮点数运算时IEEE 754规范中的默认舍入模式,称为向最接近数舍入模式

在把浮点数转换为整数时使用IEEE 754标准中的向零舍入模式

Java虚拟机在处理浮点数运算时,不会抛出任何运行时异常。溢出时,将会使用有符号的无穷大来表示,如果某个操作结果没有明确的数学定义的话,将会时候NaN值来表示

在对long类型数值进行比较时,虚拟机采用带符号的比较方式,采用IEEE 754规范说定义的无信号比较(Nonsignaling Comparisons)方式。

2.11.4 类型转换指令

类型转换指令可以将两种Java虚拟机数值类型进行相互转换,这些转换操作一般用于实现用户代码的显式类型转换操作,或者用来处理Java虚拟机字节码指令集中指令非完全独立独立的问题

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。

尽管可能发生上限溢出、下限溢出和精度丢失等情况,但是Java虚拟机中数值类型的窄化转换永远不可能导致虚拟机抛出运行时异常

NAN --> 0(int) NAN-->NAN(float)

2.11.5 对象创建与操作

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

2.11.6 操作数栈管理指令

Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2和swap。

2.11.7 控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件地从指定指令而不是控制转移指令的下一条指令继续执行程序。

  • 条件分支: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

由于各种类型的比较最终都会转化为int类型的比较操作,基于int类型比较的这种重要性,Java虚拟机提供了非常丰富的int类型的条件分支指令。

所有int类型的条件分支转移指令进行的都是有符号的比较操作。

2.11.8 方法调用和返回指令

  • invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。

  • invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

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

  • invokestatic指令用于调用类方法(static方法)。

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

2.11.9 抛出异常

显式抛出异常的操作会由athrow指令实现

2.11.10 同步

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

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作)之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。

同步一段指令集序列通常是由Java语言中的synchronized块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持。需要编译器与Java虚拟机两者协作支持。

结构化锁定(Structured Locking)是指在方法调用期间每一个管程退出都与前面的管程进入相匹配的情形。

假设T代表一条线程,M代表一个管程的话:

  • T在方法执行时持有管程M的次数必须与T在方法完成(包括正常和非正常完成)时释放管程M的次数相等。

  • 找方法调用过程中,任何时刻都不会出现线程T释放管程M的次数比T持有管程M次数多的情况。

2.12 类库

Java虚拟机必须对不同平台下Java类库的实现提供充分的支持,因为其中有一些类库如果没有Java虚拟机的支持的话是根本无法实现的。

可能需要Java虚拟机特殊支持的类库包括有:

  • 反射,譬如在java.lang.reflect包中的各个类和java.lang.Class类
  • 类和接口的加载和创建,最显而易见的例子就是java.lang.ClassLoader类
  • 类和接口的链接和初始化,上一点的例子也适用于这点
  • 安全,譬如在java.security包中的各个类和java.lang.SecurityManager等其他类
  • 多线程,譬如java.lang.Thread类
  • 弱引用,譬如在java.lang.ref包中的各个类

2.13 公有设计,私有实现

(第二章 Java虚拟机结构 全部部分小节) 本书简单描绘了Java虚拟机应有的共同外观:Class文件格式以及字节码指令集等。

Java虚拟机实现必须能够读取Class文件并精确实现包含在其中的Java虚拟机代码的语义。

只要优化后Class文件依然可以被正确读取,并且包含在其中的语义能得到保持,那实现者就可以选择任何方式去实现这些语义,虚拟机后台如何处理Class文件完全是实现者自己的事情,只要它在外部接口上看起来与规范描述的一致即可

虚拟机实现的方式主要有以下两种:

  • 将输入的Java虚拟机代码在加载时或执行时翻译成另外一种虚拟机的指令集
  • 将输入的Java虚拟机代码在加载时或执行时翻译成宿主机CPU的本地指令集(有时候被称Just-In-Time代码生成或JIT代码生成)

说明