6.4 字节码指令简介
- Java虚拟机指令由一个操作码和零至多个操作数构成
- 操作码是长一个字节、代表某种特定操作含义的数字
- 操作数是此操作需要的参数
- Java虚拟机采用面向操作数栈 的架构,故大多数的指令都不包含操作数,只有一个操作码
- 由于Java虚拟机操作码的长度为一个字节,故操作码总数不超过256条,当虚拟机处理超过一个字节的数据时,需要在运行时从字节中重建出具体数据的结构,因此可能会影响性能
- Java虚拟机解释器基本执行模型伪代码(未考虑异常处理):
do {
自动计算PC寄存器的值加1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if (字节码存在操作数) 从字节码流中读出操作数;
执行操作码所定义的操作;
} while(字节码流长度 > 0);
- Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。
- 对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。
- 但部分指令的助记符中没有明确地指明操作类型的字母,如arraylength,它的操作数只能是数组。
- 部分指令与数据类型无关,如无条件跳转指令goto
- Java虚拟机指令集被故意设计成非独立的。
- 大部分的指令都没有支持整数类型byte、char和short,甚至没有任何支持boolean类型,与boolean、byte、char和short类型数据的操作,都是用相应的int类型作为运算类型。
- 编译器会在编译期或运行期将byte和short类型的数据带符号扩展为int类型数据
- boolean和char类型数据零位扩展为相应的int类型数据
- 数组处理时,boolean、byte、short和char类型处理与上述相同
- 加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,下面的以尖括号结尾的指令表示一组指令,这些指令都是某个带有一个操作数的通用指令的特殊形式(省略掉了显式的操作数),不需要进行区操作数的动作,实际上操作数就隐含在指令中。
- xload(x代表类型,具体见第六条)指令(xload、xload_<n>)将一个局部变量加载到操作数栈
- xstore(x代表类型,具体见第六条)指令(xstore、xstore_<n>)将一个值从操作数栈存储到局部变量表
- bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_null_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>指令将一个常量加载到操作数栈
- wide指令是扩充局部变量表的访问索引
- 存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作,另外,访问对象的字段或数组元素的指令也会向操作数栈传输数据。
- 运算或算数指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶。
- 没有直接支持byte、char、short和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虚拟机规范仅规定了只有当除法指令idiv、ldiv及求余指令irem、lrem中出现除数为零时,虚拟机才会跑出ArithmeticException,其余任何整型数据运算都不应该抛出运行时异常。
- Java虚拟机处理浮点数运算规则
- 完全支持IEEE 754中定义的非正规浮点数值和逐级下溢的运算规则。
- 结果必须舍入到适当的精度,非精确的结果必须舍入为可被表示的最接近的精确值,若有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的,即IEEE 754标准中的向最接近数舍入模式
- 当把浮点数转换为整数时,使用IEEE 754标准中的向零舍入模式,即舍入到最接近但不大于原值的数字。
- 处理浮点数运算时不会跑出任何运行时异常,当一个操作产生溢出时,用有符号的无穷大表示,若某个操作没有明确的数学定义,则使用NaN表示,所有使用NaN值作为操作数的算数操作,结果都返回NaN。
- 对long类型数值进行比较时,虚拟机采用带符号的比较方式,对浮点数比较则采用IEEE 754定义的无信号比较方式。
- 宽化类型转换无需显示的转换指令:
- int类型可自动转为long、float或double
- long类型可自动转为float、double
- float类型可自动转为double
- 窄化类型转换必须显式地使用转换指令来完成:
- 指令包含:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f
- 可能导致正负号不同、数量级不同、精度丢失等
- int或long窄化为整数类型T时,简单的丢弃高位,所以可能导致正负号不同
- 浮点值窄化为整数类型T(int或long)时:
- 若浮点数为NaN,则转换结果为0
- 若浮点值不是无穷大,则向零舍入取整为v,此时若T的范围能包含v,结果为v,否则根据v的符号,转换为T的最大或者最小正数
- double窄化为float使用向最接近数舍入模式舍入到float类型可表示的数字。若转换结果的绝对值太小,则返回float类型的正负零;若转换结果的绝对值太大而无法使用float表示,则返回float类型的正负无穷大,对应double类型的NaN值将按规定转换为float类型的NaN值。
- Java虚拟机对类实例和数组的创建使用不同的字节码指令。
- 对象创建与访问指令:
- 创建类:new
- 创建数组:newarray、anewarray、multianewarray
- 访问类字段:getstatic、putstatic
- 访问实例字段:getfield、putfield
- 数组元素加载到操作数栈:Taload(baload、caload、saload、iaload、laload、faload、daload、aaload)
- 操作数栈值存储到数组元素:Tastore(bastore、castore、sastore、iastore、lastore、fastore、dastore、aastore)
- 取数组长度:arraylength
- 检查类实例类型的指令:instanceof、checkcast
- 操作数栈管理指令:
- 将操作数栈栈顶的一个或两个元素出栈:pop、pop2
- 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
- 将栈最顶端的两个数值交换:swap
6.5 公有设计和私有设计
- Java虚拟机应有的共同存储格式包含了:
Class文件格式
及字节码指令集
。 - 虚拟机实现的主要方式:
- 将输入的Java虚拟机代码在加载或执行时翻译成另一种虚拟机的指令集
- 将输入的Java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(即JIT代码生成技术)