介绍java虚拟机的指令功能,至少能阅读java代码生成的字节码指令含义

一、概述

Java虚拟机采用基于栈的架构,其指令由操作码和操作数组成。

  • 操作码:一个字节长度(0~255),意味着指令集的操作码个数不能操作256条。
  • 操作数:一条指令可以有零或者多个操作数,且操作数可以是1个或者多个字节。编译后的代码没有采用操作数长度对齐方式,比如16位无符号整数需使用两个字节储存(假设为byte1和byte2),那么真实值是 (byte1 << 8) | byte2

放弃操作数对齐操作数对齐方案:

  • 优势:可以省略很多填充和间隔符号,从而减少数据量,具有更高的传输效率;Java起初就是为了面向网络、智能家具而设计的,故更加注重传输效率。
  • 劣势:运行时从字节码里构建出具体数据结构,需要花费部分CPU时间,从而导致解释执行字节码会损失部分性能。

二、指令

大多数指令包含了其操作所对应的数据类型信息,比如iload,表示从局部变量表中加载int型的数据到操作数栈;而fload表示加载float型数据到操作数栈。由于操作码长度只有1Byte,因此Java虚拟机的指令集对于特定操作只提供有限的类型相关指令,并非为每一种数据类型都有相应的操作指令。必要时,有些指令可用于将不支持的类型转换为可被支持的类型。

对于byte,short,char,boolean类型,往往没有单独的操作码,通过编译器在编译期或者运行期将其扩展。对于byte,short采用带符号扩展,chart,boolean采用零位扩展。相应的数组也是采用类似的扩展方式转换为int类型的字节码来处理。 下面分门别类来介绍Java虚拟机指令,都以int类型的数据操作为例。

栈是指操作数栈

2.1 栈操作相关

load和store

  • load 命令:用于将局部变量表的指定位置的相应类型变量加载到栈顶;
  • store命令:用于将栈顶的相应类型数据保入局部变量表的指定位置;
变量进栈含义变量保存含义
iload第1个int型变量进栈istore栈顶int数值存入第1局部变量
iload_0第1个int型变量进栈istore_0栈顶int数值存入第1局部变量
iload_1第2个int型变量进栈istore_1栈顶int数值存入第2局部变量
iload_2第3个int型变量进栈istore_2栈顶int数值存入第3局部变量
iload_3第4个int型变量进栈istore_3栈顶int数值存入第4局部变量
lload第1个long型变量进栈lstore栈顶long数值存入第1局部变量
fload第1个float型变量进栈fstore栈顶float数值存入第1局部变量
dload第1个double型变量进栈dstore栈顶double数值存入第1局部变量
aload第1个ref型变量进栈astore栈顶ref对象存入第1局部变量

const、push和ldc

  • const、push:将相应类型的常量放入栈顶
  • ldc:则是从常量池中将常量
常量进栈含义
aconst_nullnull进栈
iconst_m1int型常量-1进栈
iconst_0int型常量0进栈
iconst_1int型常量1进栈
iconst_2int型常量2进栈
iconst_3int型常量3进栈
iconst_4int型常量4进栈
iconst_5int型常量5进栈
lconst_0long型常量0进栈
fconst_0float型常量0进栈
dconst_0double型常量0进栈
bipushbyte型常量进栈
sipushshort型常量进栈
常量池操作含义
ldcint、float或String型常量从常量池推送至栈顶
ldc_wint、float或String型常量从常量池推送至栈顶(宽索引)
ldc2_wlong或double型常量从常量池推送至栈顶(宽索引)

pop和dup

  • pop用于栈顶数值出栈操作;
  • dup用于赋值栈顶的指定个数的数值,并将其压入栈顶指定次数;
栈顶操作含义
pop栈顶数值出栈(不能是long/double)
pop2栈顶数值出栈(long/double型1个,其他2个)
dup复制栈顶数值,并压入栈顶
dup_x1复制栈顶数值,并压入栈顶2次
dup_x2复制栈顶数值,并压入栈顶3次
dup2复制栈顶2个数值,并压入栈顶
dup2_x1复制栈顶2个数值,并压入栈顶2次
dup2_x2复制栈顶2个数值,并压入栈顶3次
swap栈顶的两个数值互换,且不能是long/double

注意:dup2对于long、double类型的数据就是一个,对于其他类型的数据,才是真正的两个,这个的2代表的是2个slot的数据。

2.2 对象相关

字段调用

字段调用含义
getstatic获取类的静态字段,将其值压入栈顶
putstatic给类的静态字段赋值
getfield获取对象的字段,将其值压入栈顶
putfield给对象的字段赋值

方法调用

方法调用作用解释
invokevirtual调用实例方法虚方法分派
invokestatic调用类方法static方法
invokeinterface调用接口方法运行时搜索合适方法调用
invokespecial调用特殊实例方法包括实例初始化方法、父类方法
invokedynamic由用户引导方法决定运行时动态解析出调用点限定符所引用方法

方法返回

方法返回含义
ireturn当前方法返回int
lreturn当前方法返回long
freturn当前方法返回float
dreturn当前方法返回double
areturn当前方法返回ref

对象和数组

  • 创建类实例: new
  • 创建数组:newarray、anewarray、multianewarray
  • 数组元素 加载到 操作数栈:xaload (x可为b,c,s,i,l,f,d,a)
  • 操作数栈的值 存储到数组元素: xastore (x可为b,c,s,i,l,f,d,a)
  • 数组长度:arraylength
  • 类实例类型:instanceof、checkcast

2.3 运算指令

运算指令是用于对操作数栈上的两个数值进行某种运算,并把结果重新存入到操作栈顶。Java虚拟机只支持整型和浮点型两类数据的运算指令,所有指令如下:

运算intlongfloatdouble
加法iaddladdfadddadd
减法isublsubfsubdsub
乘法imullmulfmuldmul
除法idivldivfdivddiv
求余iremlremfremdrem
取反ineglnegfnegdneg

其他运算:

  • 位移:ishl,ishr,iushr,lshl,lshr,lushr
  • 按位或: ior,lor
  • 按位与: iand, land
  • 按位异或: ixor, lxor
  • 自增:iin
  • 比较:dcmpg,dcmpl,fcmpg,fcmpl,lcmp

2.4 类型转换

类型转换用于将两种不同类型的数值进行转换。

(1) 对于宽化类型转换(小范围向大范围转换),无需显式的转换指令,并且是安全的操作。各种范围从小到大依次排序: int, long, float, double。

(2)对于窄化类型转换,必须显式地调用类型转换指令,并且该过程很可能导致精度丢失。转换规则中需要特别注意的是当浮点值为NaN, 则转换结果为int或long的0。虽然窄化运算可能会发生上/下限溢出和精度丢失等情况,但虚拟机规范明确规定窄化转换U不可能导致虚拟机抛出异常。

类型转换指令:i2b, i2c,f2i等等。

2.5 流程控制

控制指令是指有条件或无条件地修改PC寄存器的值,从而达到控制流程的目标

  • 条件分支:ifeq、iflt、ifnull、ifnonnull等
  • 复合分支:tableswitch、lookupswitch
  • 无条件分支:goto、goto_w、jsr、jsr_w、ret

2.6 同步与异常

异常:

Java程序显式抛出异常: athrow指令。在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现,而是采用异常表来完成。

同步:

方法级的同步和方法内部分代码的同步,都是依靠管程(Monitor)来实现的。

Java语言使用synchronized语句块,那么Java虚拟机的指令集中通过monitorenter和monitorexit两条指令来完成synchronized的功能。为了保证monitorenter和monitorexit指令一定能成对的调用(不管方法正常结束还是异常结束),编译器会自动生成一个异常处理器,该异常处理器的主要目的是用于执行monitorexit指令。

2.7 小结

在基于堆栈的的虚拟机中,指令的主战场便是操作数栈,除了load是从局部变量表加载数据到操作数栈以及store储存数据到局部变量表,其余指令基本都是用于操作数栈的。