JVM-字节码指令
标签:JVM

字节码指令

每一个Java字节码指令是一个byte数字,并且有一个对应的助记符。目前所有的字节码指令大约有200个。一个方法的Java字节码被编译到Code属性中,如果要查看指令的具体内容,可以使用Javap。

bipushsipush 都是将操作数压入操作数栈,不过bipush只能接受一个字节(-128~127),sipush可以支持(-32768~32767)。虚拟机通过这种细分的指令集,可以尽可能的减少指令所占的空间。

store指令表示从操作数栈中弹出一个元素,将其存放在局部变量表中。一般来说,类似store这样的指令需要带一个参数,用来指明将弹出的元素放在局部变量表的第几个位置。但是为了尽可能的压缩指令的大小,专门使用 istore_0istore_1istore_2istore_3表示将弹出的操作数放到局部变量表常用的第0,1,2,3的位置上。如果超过了3的话,就使用 istore 后跟位置。

load指令将局部变量表第一个位置的值压入操作数栈。和store指令类似,也有 iload_0iload_1,iload_2iload_3分别表示将局部变量表的第0,1,2,3的位置压入操作数栈。如果要操作比较大的局部变量表,可以使用iload` 加一个局部变量表位置。

iadd 表示加法操作,它从操作数栈中弹出两个元素做加法,并将结果再压回操作数栈。

idiv 表示除法操作,不过除法有顺序,是将栈顶第2顺位的元素除以栈顶元素。

ireturn将当前函数操作数栈的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中。

1. 常用入栈指令

入栈指令根据数据类型和内容不同又分为const系列,push系列和ldc系列。

const系列:

const系列用于对特定的常量入栈,入栈的常量隐含在指令本身里。

i表示整数,l表示长整数,f表示浮点数,d表示双精度浮点数,a表示对象引用。隐含的操作数会用下划线形式给出。

push系列:

ldc系列:

2. 局部变量压栈指令

主要分为

3. 出栈装入局部变量表指令

主要分为

4. 通用性操作

NOP:它的字节码为0x00,表示什么都不做,这条指令一般可用于调试,占位等

dup:意为duplicate,它会将栈顶元素复制一份再次压入栈顶,这样栈顶就有两份一样的元素

pop:把一个元素从栈顶弹出,直接废弃掉。

pop指令丢弃一个字长(32位),如果要丢弃栈顶64位数据(long或double)要使用pop2,类似的dup也有dup2

5. 类型转换指令

x2y:x为(i,f,l,d),y为(i,f,l,d,c,s,b)

注意上面没有从byte,char或short转换为其他数据类型的指令,原因有:

  1. 因为虚拟机只愿意使用单字节表示指令,最多不能超过256个,如果为byte等都准备一套指令,则会超过
  2. 由于局部变量的槽位固定为32位,故,byte,short存入都会占用32位空间,故可以直接使用int类型 命令就可以。

6. 运算指令

x为,f,d,l

7. 对象/数组操作指令

创建指令:

new:结束一个操作数表示指向常量池的索引,表示要创建的类型。

newarray:创建基本类型的数组

anewarray:创建对象数组

字段访问指令:

getfieldputfield:操作实例对象字段

getstaticputstatic:操作静态字段

类型检查指令:

checkcast:检查强制类型转换是否可以进行

instanceof:判断给定对象是否是某一个类的实例

数组操作指令:

xastroe

xaload

arraylenth:获取数组的长度

8. 比较控制指令

比较指令:比较栈顶两个元素的大小,并将比较结果入栈

dcmpgdcmpl

fcmpgfcmpl

lcmp

d表示double,f表示float,l表示long,由于double和float存在NaN存在,故存在两个版本,故存在 ~cmpg~cmpl ,它们都从栈顶弹出两个操作数,并将它们比较,设栈顶元素为v2,栈顶顺位第2位为v1,

区别再去如果遇到NaN,fcmpg会压入1,而fcmpl压入-1。

条件跳转指令:

ifeq

iflt

ifle

ifne

ifgt

ifge

ifnull

ifnonnull

上面这些指令和汇编中的指令都相似,并且上面这些指令都接收两个字节的操作数,用于计算跳转的位置,含义都是:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,跳转到给定位置。

比较条件跳转指令

if_icmpeq

if_icmpne

if_icmplt

if_icmpgt

if_icmple

if_icmpge

if_acmpeq

if_acmpne

上面这些指令以i开头表示对整数(包括short,byte),以a开头表示对象引用。这些指令都接收两个两个字节的操作数作为参数,用于计算跳转位置。同时在执行时,栈顶要准备两个元素进行比较,指令执行结束后,两个元素被清空,且没有任何数据入栈,如果条件成立,则执行跳转,否者继续执行下一条语句。

多条件分支跳转:

多条件分支是为switch-case设计的,包括了tablesswitchlookupswitch

区别在于:

tableswitch要求多个条件的分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量上,因此效率较高。

lookupswitch内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并且根据对应的offset计算跳转地址,因此效率较低。

上面可见switch中的case值是连续的,故用 Javap -v Test 查看的时候显示的是tableswitch。

对于不连续的值,使用的是 lookupswitch

由于从JDK 1.7 支持字符串比较,switch先调用字符串的hashCode()方法得到int整数比较,由于存在hash冲突,再对字符串的内容进行equals比较,这样才算是匹配了。由于过程复杂,故性能比int匹配要差。

无条件跳转指令:

goto接收两个字节操作数,如上上面的 goto 57 ,表示跳转到的位置,如果要跳转的位置操作了两个字节,可以使用 goto_w 它可以结束四个字节的数字作为跳转位置。

9. 函数调用与返回指令

invokevirtual:虚函数调用,调用对象的实例方法,根据对象的实际类型进行派发,支持多态,也是最常见的Java函数调用方式。

invokeinterface:指接口方法的调用,当被调用对象声明为接口时,使用该指令调用接口的方法。

invokespecial:调用一些特殊的方法,比如构造函数,类的私有方法,父类方法。这些方法都是静态类型绑定我的,不会再调用时进行动态派发。

invokestatic:调用类的静态方法,这个也是静态绑定的。

invokedynamic:调用动态绑定的方法。

函数调用结束前,要进行返回,返回时,需要使用xreturn指令将返回值存入调用者的操作数栈中,根据返回值类型不同,前缀x也不同,int为i,返回void,则直接为return。如果方法是同步的,调用后监视器锁将被释放。

  • 9 min read

CONTRIBUTORS


  • 9 min read