6 字节码指令简介
目录
1 学习总结
2 关于字节码指令
本章内容不深入讲解指令的执行过程,只对指令列表的及其基本功能说明。
2.1 指令组成
字节码指令:指令编码+操作数组成。如下图所示:
- 指令编码用二进制数来表示,分配1字节大小。因此字节码指令最多有2^8,即256条。
- 操作数不使用对齐补零方式填充,因此如果超过1字节的数据,可能拆解成N部分来存储。如下所示:
譬如要将一个16位长度的无符号整数使用两个无符号字节存储起来(假设将它们命名为byte1和byte2),那它们的值应该是这样的:(byte1 << 8) | byte2
举例:
原数:1111 0000 0000 1111
拆解后:
byte1:1111 0000
byte2 :0000 1111
(byte1 << 8) | byte2 = (1111 0000)<<8 | (0000 1111) = 111 0000 000 1111
字节码指令设计方式优缺点:
- 放弃了操作数长度对齐,省略掉大量的填充和间隔符号
- 用一个字节来代表操作码,编译代码变得更加短小精干。求尽可能小数据量、高传输效率的设计是由Java语言设计之初主要面向网络、智能家电的技术背景所决定的,并一直沿用至今。
- 缺点:解释执行字节码时增加了运算量,降低性能。
2.1 基本执行模型
可以用下面的这段伪代码来描述虚拟机执行字节码的过程:(不考虑异常)
do {
PC寄存器的值加1;
根据PC寄存器指示的位置,从字节码流中取出操作码; //取指
if (字节码存在操作数)
从字节码流中取出操作数; //译码
执行操作码所定义的操作; // 执行
} while (字节码流长度 > 0);
3 字节码指令与数据类型
3.1 指令编码与数据类型相关性
- 大多数指令都包含数据类型信息,例如:iload指令表示加载int型的数据,fload指令加载loat类型。
- 操作码助记符:表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表 float,d代表double,a代表reference。
- 指令集:对于相同的执行内容,但是操作不同数据类型那些指令,可用指令集来概括表示。例如Tipush指令集包括bipush(int类型)和sipush(short类型)两个指令
3.2 指令不支持的数据类型转换
- 基于指令数量的限制,并非每种数据类型和每一种操作都有对应的指令。对于没有指令支持的操作数类型,编译器会在编译期或运行期将该操作数扩展成可支持的类型,例如将byte和short类型的数据带符号扩展为相应的int类型数据,将boolean和char类型数据零位扩展为相应的int类型数据。(扩展过程暂不展开分析)
4 指令的分类说明
4.1 加载和存储指令
功能:在局部变量表和操作数栈之间执行数据的加载和写回。指令列表分类如下:
指令功能 | 指令列表 |
---|---|
将一个局部变量加载到操作栈 | iload、iload_ |
将一个数值从操作数栈存储到局部变量表 | istore、istore_ |
将一个常量加载到操作数栈 | bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、 iconst_、lconst_ |
扩充局部变量表的访问索引的指令 | wide |
注意:尖括号结尾的,它际上代表了一组指令(例如iload_
4.2 运算指令
功能:对两个操作数栈上的值进行某种运算,并把结果重新存入到栈顶。
(关于入栈出栈的说明,可转入《深入理解计算机系统》专题学习)
指令功能 | 指令列表 |
---|---|
加法 | iadd、ladd、fadd、dadd |
减法 | isub、lsub、fsub、dsub |
乘法 | imul、lmul、fmul、dmul |
除法 | idiv、ldiv、fdiv、ddiv |
求余 | irem、lrem、frem、drem |
取反 | ineg、lneg、fneg、dneg |
位移 | ishl、ishr、iushr、lshl、lshr、lushr |
按位或 | ior、lor |
按位与 | iand、land |
按位或 | ior、lor |
按位异或 | ixor、lxor |
局部变量自增 | iinc |
比较 | dcmpg、dcmpl、fcmpg、fcmpl、lcmp |
4.3 类型转换指令
功能:将两种不同的数值类型相互转换
例如:
- 用户代码中的显式类型转换操作
- 指令不支持的数据类型转换
关于宽化/窄化转换类型对比:
宽化 | 窄化 | ||
---|---|---|---|
定义 | 小范围类型向大范围类型,例如int转long | 宽化的相反过程 | |
是否需要指令完成 | 不需要 | 需要显式指定转换指令 | |
是否精度丢失 | 不丢失 | 因为可能溢出而导致符号或者精度丢失 | |
是否抛异常 | 无异常 | 数值类型的窄化转换指令不会抛出运行时异常 | |
宽化类型转换:小范围类型向大范围类型的安全转换,例如int转long | |||
窄化类型转换:宽化的相反过程 |
4.4 对象创建与访问指令
创建指令:JVM对类实例和数组的创建与操作使用了不同的字节码指令
访问指令:对象创建后,通过对象访问指令获取对象实例中的字段或者数组实例中的元素
指令功能 | 指令列表 |
---|---|
创建类实例 | new |
创建数组 | newarray、anewarray、multianewarray |
访问类变量 | getstatic、putstatic |
访问实例变量 | getfield、putfield |
把数组元素加载到操作数栈 | baload、caload、saload、iaload、laload、faload、 daload、aaload |
将一个操作数栈的值储存到数组元素中 | bastore、castore、sastore、iastore、fastore、 dastore、aastore |
取数组长度 | arraylength |
检查类实例类型的指令 | instanceof、checkcast |
4.5 操作数栈管理指令
功能:操作操作数栈
指令功能 | 指令列表 |
---|---|
将操作数栈的栈顶一个或两个元素出栈 | pop、pop2 |
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶 | dup、dup2、dup_x1、 dup2_x1、dup_x2、dup2_x2 |
将栈最顶端的两个数值互换 | swap |
4.6 控制转移指令
功能:判断并地修改PC寄存器的值。
指令功能 | 指令列表 |
---|---|
条件分支 | ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、 if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne |
复合条件分支 | tableswitch、lookupswitch |
无条件分支 | goto、goto_w、jsr、jsr_w、ret |
4.7 方法调用和返回指令
方法调用指令如下:
指令 | 指令功能 |
---|---|
invokevirtual | 用于调用对象的实例方法,根据对象的实际类型进行分派 |
invokeinterface | 用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用 |
invokespecial | 用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法 |
invokestatic | 用于调用类静态方法 |
invokedynamic | 在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法 |
方法返回指令:
- 方法返回指令是根据返回值的类型区分的
- ireturn(当返 回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn
- return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用
4.8 异常处理指令
指令 | 指令功能 |
---|---|
athrow | 显式抛出异常的 |
4.9 同步指令
monitorenter和monitorexit两条指令来支持synchronized关键字。
详细参考《3、Java并发 - synchronized详解》
- jvm从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法。
- 当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有锁,然后才能执行方法
- 最后当方法完成(无论是正常完成 还是非正常完成)时释放锁。
- 在方法执行期间,执行线程持有了锁,其他任何线程都无法再获取到同一个锁。
- 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同 步方法所持有的锁将在异常抛到同步方法边界之外时自动释放。
字节码例子说明同步指令:
void onlyMe(Foo f){
synchronized(f){
doSomething();
}
}
字节码指令部分如下:
注意:为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理程序,这个异常处理程序声明可处理所有的异常,它的目的就是用来执行monitorexit指令。