2.字节码指令集与解析举例
一、概述
-
Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行命令
-
Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的数字(称为操作码:Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数:Operands)构成
-
由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码
-
由于限制了Java虚拟机操作码的长度为一个字节(即0 ~ 255),这意味着指令集的操作码总数不可能超过 256 条
-
官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html
二、执行模型
- 如果不考虑异常处理的话,那么Java虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型来理解
三、字节码与数据类型
-
在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息
-
例如:iload指令用于从局部变量表中加载int类型的数据到操作数栈中,而fload指令加载的则是float类型的数据
-
大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型
四、指令的分类
- 加载与存储指令
- 算术指令
- 类型转换指令
- 方法调用与返回指令
- 操作数栈管理指令
- 控制转移指令
- 异常处理指令
- 同步控制指令
五、加载与存储指令
5.1、作用
- 加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递
5.2、常用指令
-
1、从局部变量压入操作数栈指令
将局部变量表中对应索引的变量压入操作数栈栈顶
xload:(其中x 为 i、l、f、d、a)
xload_<n> (其中x 为 i、l、f、d、a,n为0到3)
x为数据类型
n表示局部变量表索引的位置
当局部变量表索引大于3时,就使用xload index指令 -
2、常量入栈指令
将一个常量加载到操作数栈,分为const系列、push系列和ldc系列
-
- const:
用于对特定的常量压入操作数栈,入栈的常量隐含在指令本身里
iconst_<i>(第二个i从 负1 到5)
lconst_<l>(第二个l从0到1)
fconst_<f>(第二个f从0到2)
dconst_<d>(第二个d从0到1)
aconst_null(长线是下划线)
从指令的命名上不难找出规律,指令助记符的第一个字符总是喜欢表示数据 类型,i表示整数、l表示长整型、f表示浮点数、d表示双精度浮点,习惯上 用a表示对象引用。如果指令隐含操作的参数,会以下划线形式给出
- const:
-
- push:
主要包括bipush和sipush,它们的区别在于接受数据类型的不同
bipush接收8位整数作为参数,将参数压入栈
sipush接收16位整数,将参数压入栈
- push:
-
- ldc:
如果以上指令都不能满足需求,那么可以使用万能的ldc指令,它可以接收一个8位的参数,该参数指向常量池中的int、float或者String的索引,将指定的内容压入堆栈
- ldc:
-
- ldc-w:
它接收两个8位参数,能支持的索引范围大于ldc如果要压入的元素是long或者double类型的,则使用ldc2-w指令,使用方式都是类似的
- ldc-w:
-
3、出栈与局部变量表指令
弹出操作数栈栈顶元素,存储到局部变量表对应索引处,给局部变量赋值
xstore(x 为i、l、f、d、a):指令xstore由于没有隐含参数信息,故需要提供一个byte类型的参数类指定目标局部变量表的位置
xstore_<n>(其中x为i、l、f、d、a,n为0到3):弹出操作数栈对应栈顶元素,为局部变量表对应索引处的局部变量进行赋值
六、算术指令
-
用于对两个操作数梭上的值进行某种特定运算,并把结 果重新压入操作数。
-
分类 大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮 直类型数据进行运算的指令。
-
注意:byte、 short、char和 boolean类型说明在每一大类中,都有针对Java虚拟机具体数据类型的专用算术指令。但没有直接支持byte、 short、char和 boolean类型的算术指令,对于这些数据的运算,都使用int类型的指令来处理。此外,在处理 boolean、byte、 short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。
6.1、各种指令
NaN值使用
当一个操作产生溢出时,将会使用有符号的无穷大表示,如果
某个操作结果没有明确的数学的话,将会使用NaN值来表示。而
且所有使用NaN值作为操作数的算术操作,结果都会返回NaN
所有的算术指令包括:
加法指令:iadd ladd、fadd、dadd
减去指令:isub、lsub、fsub、dsub
乘法指令:imul、lmul、fmul、dmul
除法指令:idiv、ldiv、fdiv、ddiv
求余指令:irem、lrem、frem、drem // remainder:余数
取反指令:ineg、lneg、fneg、dneg // negation:取反
自增指令:iinc
位运算指令,又可分为
位移指令:ishl、ishr、 iushr、lshl、lshr、lushr
按位或指令:ior、lor
按位与指令:iand、land
按位异或指令:ixor、lxor
比较指令: dcmpg、dcmpl、 fcmpg、fcmpl、lcmp
6.1.1、++i与i++的区别
++i,代码分析
i++,代码分析
总结:可以看出如果没有任何操作的话,他俩的字节码是完全相同的,如果有操作:那么++i就是在局部变量表里面+1之后放入操作数栈;i++的话就是先把原数据放入操作数栈,然后在对局变量表进行+1;
七、对象的创建于访问指令
- 对象的创建与访问指令 Java是面向对象的程序设计语言,虚拟机平台从字节码层 面就对面向对象做了深层次的支持。有一系列指令专门用于对 象操作,可进一步细分为创建指令、字段访问指令、数组操作 指令、类型检查指令。
7.1、对象的创建
虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创
建与操作使用了不同的字节码指令:
- 1 创建类实例的指令:创建类实例的指令 new它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。
- 2 创建数组的指令:创建数组的指令: newarray、 anewarray、 multianewarray.newarray:创建基本类型数组anewarray:创建引用类型数组multilanewarra/创建多维数组上述创建指令可以用于创建对象或者数组,由于对象和数组在Java中的广泛使用,这些指令的使用频率也非常高。
7.2、字段访问指令
对象创建后,就可以通过对象访问指令获取对象实例或数组实例
中的字段或者数组元素。
访问类字段( static字段,或者称为类变量)的指令:
getstatic、 putstatic
访问类实例字段(非 static字段,或者称为实例变量)的指令:
getfield、 putfield
举例:
以 getstatic指令为例,它含有一个操作数,为指向常量池
的Fieldref索引,它的作用就是获取 Fieldref指定的
对象或者值,并将其压入操作数栈。
public void sayhello(){
System. out.println("hello");
}
对应的字节码指令:
0 getstatic #8 <java/lang/System. out>
3 1dc #9 <hello>
5 invokevirtual #10 <java/io/Printstream println>
8 return
7.2、数组操作指令
数组操作指令主要有: xastore和 xload指令。具体为:
把一个数组元素加载到操作数栈的指令:
baload、 caload、 saload、 iaload、laload、 faload、
daload、 aaload
将一个操作数栈的值存储到数组元素中的指令:
bastore、 castore、 sastore、 iastore、lastore、
faster、 dastore、 aastore
八、方法调用与返回指令
方法调用指令: invokevirtual、 invokeinterface、
invokespecial、 invokestatic、 invokedynamic
以下5条指令用于方法调用:
- invokeqlvirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态。这也是Java语言中最常见的方法分派方式。
- invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用
- invokespecia指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发。
- invokestatic指令用于调用命名类中的类方法( static方法)。这是静态绑定的。
- invokedynamic:调用动态绑定的方法,这个是JDK1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在java虚拟机内部,而
- invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
8.1、方法返回指令
各种return
九、操作数栈管理指令
dup在操作数栈中复制一份。
十、控制转移指令
举例
指令 fcmp和fcmpl都从中弹出两个操作数,并将它们做比较, 设栈顶的元素为v2,顶顺位第2位的元素为v1,若v1=v2,则压入0: 若v1>v2则压入1:若v1<v2则压入-1. 两个指令的不同之处在于,如果遇到NaN值, fcmpg会压入1, 而fcmpl会压入-1