字节码指令
官网地址:docs.oracle.com/javase/spec…
概述
java 字节码对于java虚拟机而言,其实就好比汇编语言对于计算机,属于是基本的执行指令。
java 虚拟机的指令由一个字节长度的、代表某种特定含义的数字--->称为操作码,以及跟随其后零至多个代表此操作所需要的参数--->称为操作数构成。 其中大部分指令又只有操作码,而不包含操作数操作数。由于只有一个字节长度,所以指令集的总大小只有(0~255)256的大小,其中每一个操作码都会有对应的助记符,比如aload = 25 (0x19),0x19是class文件中的字节码,对应的助记符就是aload,该指令表示从局部变量中加载一个引用数据类型元素,压入到操作数栈中。
一个指令,可以从局部变量表,常量池,对重对象,方法调用中取得数据,这些数据(可能是值,也可能是对象的引用)被压入到操作数栈中
一个指令,也可以是从操作数栈中取出一到多个值,完成复制、加减乘除、方法传参、系统调用等等。
字节码与数据类型
在java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息,例如iload指令用户从局部变量表中加载int型的数据压入到操作数栈中,而fload指令加载的则是float类型的数据压入到操作数栈中。 对于大部分与数据类型相关的字节码指令而言,他们的操作码助记符中都能表明该指令是为哪种数据类型服务的。
| 关键字符(大部分前缀) | 代表类型 |
|---|---|
| i | 代表int |
| l | 代表long |
| f | 代表float |
| d | 代表double |
| a | 代表引用数据类型 |
| 其中大部分指令都没有支持证书类型byte,char,short,甚至没有任何指令支持boolean类型。编译器会在编译期或者运行期将byte和short类型数据带符号扩展为相应的int数据类型,将boolean和char类型数据零位扩展为相应的int数据类型。因此对于boolean,byte,short,char类型数据的操作,其实在操作数栈中都已经转化为对应的int数据类型操作了 |
加载与存储指令
1.作用 加载和存储指令用于将数据从方法栈帧的局部变量表和操作数栈中来回传递。
局部变量表压栈指令load
加载指令用户将局部变量表中指令索引位置的数据,压入到操作数栈栈顶。 对于加载指令可以这样来记忆,
?load x
其中?代表i,l,f,d,a中任意一个。 x代表操作数,就是局部变量表指定索引的位置。
比如说iload 4,就是将局部变量表索引位置为4的int类型的元素压入到操作数栈中。
其中局部变量表中索引位置为0,1,2,3时,由于比较常用,所以jvm单独为这个特殊的索引位置指定了单独的指令:
?load_x
其中?依然代表i,l,f,d,a中任意一个。x代表0,1,2,3。
常量入栈指令
常量入栈指令的功能是将常数压入操作数栈,格局数据类型和入栈内容不同,又可以分为const系列、push系列和ldc系列。
-
const系列:用于对特定的常量入栈,入栈的常量隐含在指令本身里。 |指令|说明| |---|---| |
iconst_x| x为0,1,2,3,4,5中任意一个int型数据, 将x压入到栈| |iconst_m1| 将-1压入到栈| |lconst_x| x为长整形的0,1任意值,将x压入到栈| |fconst_x| x为浮点数0,1,2任意值,将x压入到栈| |dconst_x| x为double型数据0,1任意值| |aconst_null| 将null压入操作数栈。| -
push系列 主要包含
bipush和sipush。他们的区别在于,bipush接收一个8位的整数作为参数,sipush接收16位整数,他们将参数压入栈中 -
ldc系列 如果以上指令都不满足需求,可以使用万能的
ldc指令,它接收一个8位的参数,该参数只想常量池中的int、flaot、或者String类型的索引,将制定的内容压入栈中。
出栈装入局部变量表store
store代表从操作数栈弹出一个元素,存储到局部变量表指定索引位置。 和load指令类似,store指令也有类似的公式可以记忆
?store x
或者
?store_x
其中?依然代表着i,l,f,d,a中任意一个来表示数据类型,x代表着操作数,也就是存入到局部变量表中哪一个索引位置的数,或者0,1,2,3
算术指令
概述
算数指令用于对两个操作数栈上的值进行某种特定的运算,并将结果重新压入操作数栈中去。
比如:a+b, jvm会将a的值和b的值压入操作数栈中,然后取出b和a,进行求和运算,运算得到的结果重新压入到操作数栈中。
从大体上来看可以将算术指令分为对整形和对浮点型的运算指令.
对于基础数据类型byte,short,char,boolean,再操作数栈中都将转化为对应的int类型来计算。
对于某些不恰当的运算,可能会导致运算的溢出,比如两个很大的正整数相加,结果肯能是一个负数。对于除数为0的除法操作会抛出ArithmeticException.
JVM再进行浮点型数据运算时,会要求结果舍如到适当的精度,比如讲浮点型数据转化为整形数据时,会将小数点后面的数据舍弃掉。
加减乘除等指令
| 指令类型 | 指令 |
|---|---|
| 加法指令 | iadd,ladd,fadd,dadd |
| 减法指令 | isub,lsub,fsub,dsub |
| 乘法指令 | imul,lmul,fmul,dmul |
| 除法指令 | idiv,ldiv,fdiv,ddiv |
| 求余指令 | irem,lrem,frem,drem//remainder:余数 |
| 取反指令 | ineg,leng,fneg,dneg//negation: 取反 |
| 自增指令 | iinc |
其中add,sub,mul,div,rem,neg,分别对应着加、减、乘、除、求余、取反, | |
而字符前缀的i,l,f,d则分别对应着数据类型。 | |
而,自增指令只有int类型会有,所以只有iinc,其实对于long,float,double基本类型,在代码里面依然可以使用++操作,但此时JVM只会将对应着的数据类型原值和1值压入操作数栈中。然后调用?add指令做求和运算。 |
位指令
| 指令类型 | 指令 |
|---|---|
| 位移指令 | ishl,ishr,iushr,lshl,lshr,lushr |
| 按位或指令 | ior,lor |
| 按位与指令 | iand,land |
| 按位异或 | ixor,lxor |
其中shl代表左移,shr代表右移,ushr代表无符号右移,or代表按位或。and代表按位与,xor代表按位异或。
前缀i 和 l依然代表着基本数据类型int 和 long
比较指令
-
dcmpg,dcmpl,fcmpg,fcmpl,lcmp比较指令的作用是将操作数栈栈顶的两个元素比较大小,并将比较的结果压入操作数栈中。 如指令dcmpg和dcmpl将弹出栈顶的两个double类型的数据,设栈顶的第一顺位元素为d2,第二顺位元素为d1,若两个值相等则返回0重新压入栈中,若d1>d2则返回1入栈,若d2<d1则返回-1入栈。两个指令的不同之处至于dcmpg遇到NaN问题时会返回1,而dcmpl遇到Nan问题时会返回-1.由于长整形的不存在NaN问题,所以只有一个lcmp。 -
if_acmp<cond>,if_icmp<cond>
其中if_acmp<cond>代表引用数据类型的比较,<cond>代表表达式ne和eq.
即if_acmpeq判断两个引用数据类型数据是否内存地址相等,相等则将整形1入栈,否则整形0入栈
而if_acmpne判断两个引用数据类型数据是否内存地址不相等,不相等则整形1入栈,否则整形0入栈。
而if_icmp<cond>代表整形数据类型的比较,<cond>代表的表达式就会比较多:
- eq
- ne
- lt
- le
- ge
- gt 详见比较跳转指令,这里暂不赘述
类型转换指令
类型转换指令可以将两种不同的数据类型做转换,主要指数值类型,可以分为宽化数据类型转换和窄化数据类型转换,两种都有可能发生精度损失的问题
宽化数据类型转换
-
从
int类型转换为long,float,double类型指令为i2l,i2f,i2d -
从
long类型转换为float,double类型指令为:l2f,l2d -
从
float类型转换为double类型指令为:f2d简化为以下顺序转化过程int-->long-->float-->double
窄化数据类型转换
- 从
int类型转换为byte、short、char类型指令为:i2b,i2s,i2c - 从
long类型转换为int类型指令为:l2i - 从
float类型转化为int、long类型指令为:f2i,f2l - 从
double类型转换为int、long、float类型指令为:d2i,d2l,d2f
对象的创建和访问指令
创建指令主要针对于引用数据类型。
创建指令
| 指令 | 说明 |
|---|---|
| new | 接收一个操作数,为指向常量池类信息的索引,创建完成后,将创建好的对象压入栈 |
| newarray | 创建基本数据类型数组 |
| anewarray | 创建引用数据类型数组 |
| mutltianewarray | 创建多维引用数据类型数组 |
字段访问指令
- 类字段(即static字段)访问指令:
getstatic、putstatic - 实例字段访问指令:
getfield,putfield
数组操作指令
- 数组压栈指令
对于数组而言也有压栈指令,但又有些变化,总体分为:
baload,caload,saload,iaload,laload,faload,daload,aaload,可以总结为
?aload
?代表基本数据类型,前缀为‘a’的时候代表引用数据类型,bye和boolean类型的数组公用一个baload指令
再执行?aload之前操作数栈中需要弹出两个栈顶元素:索引、和数组引用,并将该数组引用的索引位置的值压入栈中。
注意:该指令不再表示从局部变量表中取值,而是从堆空间中数组具体索引位置取值
- 数组装入指令
对于数组而言也有装入指令,但又有些变化,总体分为:
bastore,castore,sastore,iastore,lastore,fastore,dastore,aastore,可以总结为
?astore
?代表基本数据类型,前缀为‘a’的时候代表引用数据类型,bye和boolean类型的数组公用一个bastore指令。
再执行?astore之前操作数栈中需要弹出三个栈顶元素:值、索引、和数组引用,并将该值赋予数组索引的位置。
注意:该指令不再表示将值存储到局部变量表中,而是堆空间中数组具体索引位置
- 数组长度指令:
arraylength
类型检查指令
主要包含 instanceof 和 checkcast
-
指令
checkcast主要用于类型强制装换是否可以进行,如果可以进行,那么该指令对操作数栈不会有任何影响,否则抛出ClassCastExecption -
指令
instanceof用来判断一个对象是否是某一个类的实例,会将判断结果重新入栈。
方法调用与返回指令
方法调用指令
| 指令 | 说明 |
|---|---|
invokevirtual | 调用对象的实例方法 |
invokeinterface | 调用接口方法 |
invokespecial | 调用实例初始化方法(构造器)、私有方法和父类方法(super.xxx())。这些方法都是静态类型绑定类型的,不存在重写的方法 |
invokestatic | 调用类中的类方法(static方法) |
invokedynamic | 调用动态绑定的方法,这个是JDK1.7之后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都是固化在JVM虚拟机内部,而该指令的分派逻辑则是由用户的所设定的引导方法决定的。 |
方法返回指令
| 返回类型 | 返回指令 |
|---|---|
void | return |
int(boolean,char,byte,short) | ireturn |
long | lretrun |
float | fretrun |
double | dreturn |
refrence | areturn |
方法返回指令?return会将当前的操作数栈中的栈顶元素弹出,并将这个元素压入调用者的操作数栈中,而当前操作数栈中的其他元素都将被剖器。如果当前的方法是synchronized方法,还会执行一个隐式的monitorexit指令,退出临界区。
操作数管理指令
-
弹出指令
pop,pop2:表示将一个或两个槽位的元素从栈顶弹出,直接废弃 -
复制指令
dup,dup2:表示复制栈顶一个或两个槽位数值并将复制值或者复制的双份值重新压入栈顶 -
复制并插入
dup_x1,dub2_x1,dup_x2,dup2_x2指令 |指令 |插入位置|栈槽位| |----------|--------------|------------------| |dub_x1|1+1=2|即复制前栈顶2个槽位下面| |dup_x2|1+2=3|即复制前栈顶3个槽位下面| |dup2_x1|2+1=3|即复制前栈顶3个槽位下面| |dup2_x2|2+2=4|即复制前栈顶4个槽位下面| -
交换指令
swap:将栈对顶端的两个槽位互换。不包含64位数据类型(long 和 double) -
空指令
nop:神马也不干
控制转移指令
程序逻辑离不开逻辑的控制,该类型指令一般分为:条件跳转指令,比较跳转指令,多条件分支跳转和无条件跳转
条件跳转指令
条件跳转指令通常和比较指令结合使用,再条件跳转之前一般栈顶先用比较指定压入比较指令的结果,然后再进行跳转,具体如下:
| 指令 | 说明 |
|---|---|
ifeq | 栈顶元素==0时跳转 |
ifne | 栈顶元素!=0时跳转 |
iflt | 栈顶元素<0时跳转 |
ifgt | 栈顶元素>0时跳转 |
ifle | 栈顶元素<=0时跳转 |
ifge | 栈顶元素>=0时跳转 |
ifnull | 栈顶元素为null跳转 |
ifnonnull | 栈顶元素不为null时跳转 |
比较跳转指令
比较跳转指令类似于比较指令和条件跳转指令的结合体,具体如下:
| 指令 | 说明 |
|---|---|
if_icmpeq | 比较栈顶两int元素大小,相等时跳转 |
if_icmpne | 比较栈顶两int元素大小,两者不相等时跳转 |
if_icmplt | 比较栈顶两int元素大小,前者小于后者时跳转 |
if_icmple | 比较栈顶两int元素大小,前者小于等于后者时跳转 |
if_icmpgt | 比较栈顶两int元素大小,前者大于后者时跳转 |
if_icmpge | 比较栈顶两int元素大小,前者大于等于后者时跳转 |
if_acmpeq | 比较栈顶两引用数据类型元素,相等时跳转 |
if_acmpne | 比较栈顶两引用数据类型元素,不相等时跳转 |
多条件分支跳转指令
| 指令 | 说明 |
|---|---|
| tableswitch | 用于switch条件跳转,case值连续 |
| lookupswitch | 用于switch条件跳转,case值不连续 |
无条件跳转指令
| 指令 | 说明 |
|---|---|
| goto | 无条件跳转到指令偏移量的位置,接收2个字节的偏移量 |
| goto_w | 无条件跳转到指令偏移量的位,接收4个字节的偏移量 |
同步方法指令
-
方法级的同步 隐式添加同步指令,即无需指令来控制,通过方法的描述符ACC_SYNCHRONIZED来标识
-
方法内部code的同步 使用
monitorenter,monitorexit控制