一、概述
字节码对于jvm相当于机器语音对一计算机,属于基本指令。
字节码由一个字节长度的代表着某种含义的数字(操作码,Opcode),以及跟随其后的零至多个代表操作所需的参数(操作数,Operands)而构成。由于JVM采用的是操作数栈而不是寄存器的结构,所以大多数指令不包含操作数,只有一个操作码。操作码最多不超过256.
1、执行模型
2、字节码与数据模型
操作指令一般都跟所操作的数据类型有关系,例如:iload就是加载一个int类型数据,同样fload加载一个float类型数据。i代表int、l代表long、s代表short、b代表byte、c代表char、f代表float、d代表double。
还有一种没有明确指明操作类型的,如arryalength指令,她没有代表数据类型的特殊字符,但是操作数永远只能是一个数组类型的对象。
另外还有一些指令,如无条件条换goto则是和数据类型无关的。
二、加载与存储指令
加载与存储指令主要用于栈帧中局部变量表和操作数栈之间数据的传递。
- 加载:进入操作数栈,load、push、ldc、const
- 存储:保存到局部变量表,push
1、局部变量表压栈指令:将局部变量表中的某个值压入操作数栈里面
- xload_n
- xload n
举例:
2、常量入栈指令:
常量入栈指令功能是讲常量压入操作数栈,根据数据类型和入栈内容的不同,又可以分为一下几种,对应的范围依次变大:
- const系列:特定的常量入栈,常量隐藏在指令里面
- iconst_<i>:i=-1~5,iconst_m1 将-1压入操作数栈
- lconst_<l>:l=0~1,lconst_1 将1压入操作数栈
- fconst_<f>:f=0~2,fconst_1 将1压入操作数栈
- dconst_<d>:0~1,dconst_0 将0 压入到操作数栈
- aconst_null:将null压入操作数栈
- push系列:当上面的值不在指定范围的时候,就使用push系列
- bipush:8位
- sipush:16位
- ldc序列:当超出push的范围的时候,使用ldc系列,接受8位参数,配的就是常量池中的索引
3、出栈装入局部变量表指令:将操作数栈栈顶的元素弹出装入局部变量表中
这类指令主要以store形式存在,如xstore(x为i、l、f、d、e)、xstore_n(x为i、l、f、d、e,n为0~3)。
- 其中,指令istore_n将从操作数栈中弹出一个整数,并把它赋值给局部变量表索引为n的位置
- 指令xstore由于没有隐含参数信息,需要提供一个byte类型的参数类指定目标局部变量表的位置
说明:一般来说,类似像store这样命令需要带一个参数,用来指明将弹出元素放在局部变量表中的哪个位置。
举例:
- 局部变量表保存的是数据
- 操作数栈每次操作完毕都会弹出
三、算数指令
1、作用:
算数指令用于对两个操作数栈上的值进行某种特定的运算,并把结果重新压入操作数栈。
2、分类:
大体上算数指令可以分为2类:对整形数据进行运算指令,对浮点型数据进行运算指令
3、byte、short、char、boolean类型说明
在每个大类中,都有针对JVM集体类型的专用算是指令,但是没有直接支持上述四种类型的算数指令,对于这些数据的运算,都使用int指令:
4、溢出问题
数据运算可能导致益处,例如很大的两个正数相架可能是一个负数。JVM规范中没有明确规定整型数据溢出的具体结果,仅规定了在处理整型数据的时候,只有除法指令以及求余指令出现除数为0的时候的异常
5、运算模式
- 向最接近数舍入模式:JVM要求在进行浮点数计算时,所有的运算结果都必须舍入适当的精度,非精确结果必须受让人可被表示的最接近精确值,存在2个,选择最低有效位为0的。
- 向0舍入模式:将浮点数转换为整数,采用该模式。该模式将目标数值中选择一个最接近但是不大于原值的数字
6、NaN值使用
当操作产生溢出时,会讲使用有符号的无穷大表示。如果没有明确的数学定义结果,会使用NaN。
7、++运算符
- i = i+3 与 i+=3 ,后面的代码时自增,指令少,效率高
- 直接return(i+j)和 return k,前面的代码不需要在保存到本地变变量表,效率高
- ++i和i++ 在字节码层面没有任何区别,都是 iinc index by 1
- x=++i 和 x=i++,区别就是前++是先自增->操作数栈->本地变量表,后++是自增->本地变量表
四、类型转换指令
- 类型转换指令可以将两种不同的数值类型进行相互转换
- 这些转换操作一般用于实现用户代码中显式的转换操作,或者处理字节码指令集中数据类型相关无法与数据类型一一对应的问题
1、宽化类型转换:主要从小范围类型像大范围类型转换,自动提升。也不需要使用者指定转换代码。int->long->float->double
精度损失问题:
- 正常不会出现精度损失,因为是从小范围向大范围转换
- 但是从int、long转换成float,或者从long转换double 可能产生精度损失,可能丢失最低几位
byte、short、char在字节码底层,都是看做是int处理的。
2、窄化类型转换(强制类型转换):大范围向小范围转换,会产生精度损失问题,double->float->long->int
- int->byte、short、char:i2b、i2s、i2c
- long->int:l2i
- float->int、long:f2i、f2l
- double->int、long、float:d2i、d2l、d2f
五、对象的创建与访问指令
1、创建指令
- 创建类实例指令:new
- 他接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,对象的引用压入栈
- 他接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,对象的引用压入栈
- 创建数组指令
- newarray:创建基本来行数组
- anewarray:创建引用类型数组
- multianewarray:创建多位数据
2、字段访问指令
- 访问类字段(static):getstatic、putstatic
- 访问类实例字段:getfield、putfield
3、数组操作指令
数组的操作指令主要有2类:
- xaload把元素加载到操作数栈指令,要求栈顶第一个元素是数组索引i,第二个元素是数组引用a,该指令会弹出栈顶这2个元素,并将a[i]重新压入栈中
- xasotre把元素从操作数栈保存到数组指令,因为是引用数据类型,是给堆中的数组元素赋值,当使用这个元素的时候操作数栈需要准备三个值:值、数组索引、数组引用
- 其中x=b、c、s、i、l、f、d、a
- arraylength:先弹出引用,然后获取长度压入栈
举例:
4、类型检查指令
- 指令checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈,否则或抛出ClassCastException异常
- 指令instanceof用来判断给定对象是否是某一类的实例,它会将判断结果压入操作数栈
六、方法调用与返回指令
1、方法调用指令
- invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行分配(虚方法分派),支持多态。这也是java语言中最常见的方法分配方式
- invokeinterface:用于调用接口方法,他会在运行时搜索由特定对象所实现的这个接口方法,并找出合适的方法进行调用
- invokespecial:用于嗲偶用一些需要特殊处理的实例方法,如实例的初始化方法(构造器)、私有方法、父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发,属于静态绑定。
- invokestatic:用户静态方法的调用,这是静态绑定的。优先级大于invokespecial
- invokedynamic:该方法调用动态绑定方法,jdk1.7之后引入的新指令。用于在运行时动态解析出调用点限定符所引用的方法。该指令的分派逻辑是由用户所设定的引导方法决定的
2、方法返回指令
返回值指令是根据返回值类型进行具体区分的。
- 包括ireturn(boolean、byte、char、short、int)、lreturn、freturn、dreturn、areturn
- return,是返回值为void使用的
七、操作数栈管理指令
如同操作一个普通的数据结构,JVM也提供了操作数栈的管理指令,可以直接用于操作操作数栈的指令。
这类指令包括如下:
- 将一个或者两个元素弹出,并且直接废弃:pop、pop2
- 赋值栈顶一个或者两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
- 不带_x的就是直接复制 一个或者2个 Slot
- 带_x的多了一个插入操作,如dup_x1插入位置就是1+1=2,即栈顶2个Slot下面
- 将栈顶的两个Slot数值交换:swap
- 占位指令,常用调试,字节码为0X00:nop
八、控制流程指令
为了支持条件跳转,JVM提供了大量字节码指令
1、比较指令:栈顶的两个元素弹出并比较,结果压入栈,有:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
- fcmpg、fcmpl 区别在于遇到NaN的时候,fcmpg压入1、fcmpl压入-1,lcmp没有NaN,所以只有一个
2、条件跳转指令:找出栈顶的元素,测试它是否满足某一条件,如果满足条件则跳换到给定位置。条件跳转指令通常和比较指令结合使用。在跳换跳转指令执行之前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转。
举例:
因为条件跳转指令本身就适合int进行比较的,所以在比较指令中没有int类型的比较,int类型的比较直接就使用条件跳转指令进行比较了。
3、比较条件跳转指令
比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转2个指令合二为一。
4、多分支跳转指令
多分支条跳转指令主要是专门为switch-case语句设置的:
- tableswitch 要求分支条件是连续的,置存放start和end,以及若干个跳转偏移量,给定index,可以立即定位到跳转偏移量上,因此效率高
- lookupswitch 存放各个离散的case-offset,每次都要多case-offset进行操作,因此效率相对低,但是会自动排序case值,从小到大
5、无条件跳转指令
无条件跳转指令为goto,后面跟着2个字节的操作数,直接跳转到偏移量所指定的位置。指令太大的时候,就使用goto_w指令进行跳转。指令jsr、jsr_w、rst虽然也是无跳转跳转,但是主要用于try-finally语句,已经逐渐被废弃了。
九、异常处理指令
1、抛出异常指令:手动处理异常,自动或手动 throw ,指令是 athrow
athrow指令,就是显示或自动的抛出异常。
最后将异常对象放入调用者的栈帧中。例如上图0x2233地址的对象返回给调用者。
2、异常处理与异常表:抓抛模型,try-catch-finally,使用异常表
如果在方法中定义了try-catch或者try-finally结构就会创建一个异常表:
- 起始位置
- 结束位置
- 计数器记录的代码处理的偏移量
- 捕获的异常类在常量池中的索引
当一个异常被抛出时候,JVM会在当前的方法里群找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出这个栈帧。如果异常最终被处理了,则代码会继续执行。
十、同步控制指令
1、方法级的同步(同步方法)
方法级同步是隐式的,即无需通过字节码指令来控制,他实现在方法调用和返回操作之中,JVM可以从方法常量池中的ACC_SYNCHRONIZED访问标志得知一个方法是否同步。
- 同步方法,执行线程将先持有同步锁,然后执行方法。执行完毕后释放锁
- 在方法执行期间,执行线程持有同步锁,其它线程都无法获取
- 如果执行执行出现异常,在抛出同时释放锁
2、方法内指定朱令序列的同步(同步代码块)
通常指synchronized代码块,JVM指令集有monitorenter和monitorexit来支持。
在JVM中,任何对象都有都有一个监视器与之关联,判断对象是否被锁定,当监视器被持有时候,对象就是被锁定的。
当一个线程进入同步代码块的时候,使用moniterenter指令请求进入。如果当前对象监视器计数为0,则线程允许进入,如果为1则判断当前监视器的线程是否为自己,如果是则进入,否则一直等待直到对象的监视器计数为0。当线程退出代码块的时候,使用moniterexit声明退出。
执行moniterenter和moniterexit时候,都需要把对象压入栈顶,之后执行都是针对对象的监视器进行的。