JVM 三 类文件结构 下 字节码指令
字节码指令概述
在上一篇笔记的Code属性表中有一个区域是存储字节码指令的。
Java方法中的代码会被编译成一些字节码指令存储到Code属性表中。一条Java代码可能由多个字节码指令组合而成。
Java的字节码指令由两部分组成,分别是长度为一个字节的操作码,和零个或多个该操作码可需要的参数。
操作码由1个字节表示,也就是说最多也就只能够存在256种字节码指令。
字节码与数据类型
因为Java中有多种数据结构,所以必须支持各种数据结构的操作码,但是操作码只能有256个,不能全部提供支持。所以只提供了部分支持。
Java的操作码大概都是数据类型+操作的格式。如入栈一个byte类型的数据即bipush
。
如下图,对于byte
、short
类型,只提供了入栈的对应操作码,而没有提供其它操作码,boolean类型甚至没在表中。这也就解答了之前学习Java时的一些疑惑,为什么byte和short参与运算就会变成int?为什么boolean占用四字节?
加载和存储指令
Java的字节码执行系统不是基于寄存器的,是基于栈的。首先介绍下操作数栈,操作数栈和方法栈不是一个东西,操作数栈用于运算。不知道各位学习数据结构时有没有注意到栈作为计算器的一个使用案例。比如这个算式:1 + 2 + 3
首先把第一个操作数1进栈,然后读取到加号,等待第二个操作数,第二个操作数2进栈,等待中的加操作会弹出栈顶的两个元素,也就是2和1,把它们相加的结果入栈,也就是3,继续读取到加号,等待下一个操作数,下一个操作数3入栈,等待中的加操作同样弹出栈顶两个元素,也就是3和3,相加,结果6入栈。当整个合法算式读取完成,栈中唯一的元素就是结果了。
下面来介绍一些和加载存储相关的字节码指令
- 将局部变量加载到操作数栈:
iload
、iload_<n>
,lload
,lload_<n>
、fload
、fload_<n>
、dload
、dload_<n>
、aload
、aload_<n>
。 - 将一个数值从操作数栈存储到局部变量表:
istore
、istore_<n>
、同上,我不想写了 - 将一个常量加载到操作数栈:
bipush
、sipush
、ldc
、ldc_w、
ldc2_w、
aconst_null、
iconst_ml、
iconst_、
lconst_、
fconst_、
dconst_`。 - 扩充局部变量表访问索引:
wide
以尖括号结尾的,如iload_<n>
,代表了一组指令(包括iload_0,iload_1,iload_2和iload_3)。在介绍这个下划线的版本前,我先介绍iload
的完整版指令。
iload的完整版指令是iload 操作参数
,看清这里是空格,上面说过了,一个字节码由操作码和零个或多个操作参数组成,这里iload
就是操作码,这个参数代表的是局部变量的下标。而iload_<n>
这种指令则是对应的优化版,这样就可以只用一个字节的操作码来表示,省略掉操作参数。而且解析操作码和解析操作参数需要两次读内存,而用优化版本只需要一次,但优化版本只提供<type>load_<0~3>
运算指令
所以刚刚为啥我没截图呢???
类型转换指令
类型转换指令可以在两种不同的数据类型中转换。Java默认支持小数据类型自动转换成大数据类型。类型转换指令处理的则是那些大转小的操作。
提供i2b
、i2c
、i2s
、l2i、
f2i、
d2i、
d2l和
d2f`指令。
这些大转小操作会造成数据丢失,但不会引起异常。
对象创建与访问指令
操作数栈管理指令
控制转移指令
对于boolean、byte、char、short这些类型的比较都会使用int类型的指令进行比较。而对于long、float和double类型的比较则会先使用它们自己的比较运算指令(dcmpg、dcmpl、fcmpg、fcmpl、lcmp),然后返回一个整型值到操作数栈中,随后执行int类型的条件分支比较操作来完成整个分支跳转。所以Java虚拟机中大部分操作都是使用int完成的,int类型的支持极为方便强大。
方法调用指令
这部分比较重要,后面很多内容都依赖了这部分,后面也会有更详尽的介绍。
- invokevirtual指令:调用对象的实例方法,根据实际类型进行分派
- invokeinterface指令:用于调用接口方法,在运行时搜索一个实现了这个接口方法的对象,找出合适的方法进行调用
- invokespecial指令:调用一些需要特殊处理的实例方法,比如实例初始化方法,私有方法和父类方法
- invokestatic指令:调用静态方法
- invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。这是为支持动态语言特性新增的指令。使用lambda表达式时就调用了这个指令。
异常处理指令
所有显式抛出异常的操作都由athrow
指令实现
一些运行时异常会在虚拟机发现问题后自动抛出。如除零异常。
同步指令
方法级别的同步不需要字节码指令来支持,因为虚拟机在方法表中有对应的标志。而对方法中的synchronized语句块,需要字节码指令来支持。
编译器必须保证monitorenter
执行后,monitorenter
也被执行。所以可以看到下方的编译后的字节码指令,Java编译器甚至自动生成了一个异常处理路径的字节码实现来保证异常后monitorexit
也被正常执行。