6 字节码指令简介

1 学习总结

2 关于字节码指令

本章内容不深入讲解指令的执行过程,只对指令列表的及其基本功能说明

2.1 指令组成

字节码指令:指令编码+操作数组成。如下图所示:

  1. 指令编码用二进制数来表示,分配1字节大小。因此字节码指令最多有2^8,即256条
  2. 操作数不使用对齐补零方式填充,因此如果超过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 

字节码指令设计方式优缺点

  1. 放弃了操作数长度对齐,省略掉大量的填充和间隔符号
  2. 用一个字节来代表操作码,编译代码变得更加短小精干。求尽可能小数据量、高传输效率的设计是由Java语言设计之初主要面向网络、智能家电的技术背景所决定的,并一直沿用至今。
  3. 缺点:解释执行字节码时增加了运算量,降低性能。

2.1 基本执行模型

可以用下面的这段伪代码来描述虚拟机执行字节码的过程:(不考虑异常)

do {
    PC寄存器的值加1; 
    根据PC寄存器指示的位置,从字节码流中取出操作码; //取指
    if (字节码存在操作数) 
        从字节码流中取出操作数;  //译码
    执行操作码所定义的操作;   // 执行
} while (字节码流长度 > 0);

3 字节码指令与数据类型

3.1 指令编码与数据类型相关性

  1. 大多数指令都包含数据类型信息,例如:iload指令表示加载int型的数据,fload指令加载loat类型。
  2. 操作码助记符:表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表 float,d代表double,a代表reference。
  3. 指令集:对于相同的执行内容,但是操作不同数据类型那些指令,可用指令集来概括表示。例如Tipush指令集包括bipush(int类型)和sipush(short类型)两个指令

3.2 指令不支持的数据类型转换

  1. 基于指令数量的限制,并非每种数据类型和每一种操作都有对应的指令。对于没有指令支持的操作数类型,编译器会在编译期或运行期将该操作数扩展成可支持的类型,例如将byte和short类型的数据带符号扩展为相应的int类型数据,将boolean和char类型数据零位扩展为相应的int类型数据。(扩展过程暂不展开分析)

4 指令的分类说明

4.1 加载和存储指令

功能:在局部变量表和操作数栈之间执行数据的加载和写回。指令列表分类如下:

指令功能 指令列表
将一个局部变量加载到操作栈 iload、iload_、lload、lload_、fload、fload_、dload、 dload_、aload、aload_
将一个数值从操作数栈存储到局部变量表 istore、istore_、lstore、lstore_、fstore、 fstore_、dstore、dstore_、astore、astore_
将一个常量加载到操作数栈 bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、 iconst_、lconst_、fconst_、dconst_
扩充局部变量表的访问索引的指令 wide

注意:尖括号结尾的,它际上代表了一组指令(例如iload_,它代表了iload_0、iload_1、iload_2和iload_3这几条指令),表示操作数已经包含在指令中 (实际上iload_0的语义与操作数为0时的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 类型转换指令

功能:将两种不同的数值类型相互转换
例如:

  1. 用户代码中的显式类型转换操作
  2. 指令不支持的数据类型转换

关于宽化/窄化转换类型对比:

宽化 窄化
定义 小范围类型向大范围类型,例如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 运行时动态解析出调用点限定符所引用的方法,然后再执行该方法

方法返回指令:

  1. 方法返回指令是根据返回值的类型区分
  2. ireturn(当返 回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn
  3. return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用

4.8 异常处理指令

指令 指令功能
athrow 显式抛出异常的

4.9 同步指令

monitorenter和monitorexit两条指令来支持synchronized关键字。
详细参考《3、Java并发 - synchronized详解

  1. jvm从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法。
  2. 当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有锁,然后才能执行方法
  3. 最后当方法完成(无论是正常完成 还是非正常完成)时释放锁。
  4. 在方法执行期间,执行线程持有了锁,其他任何线程都无法再获取到同一个锁。
  5. 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同 步方法所持有的锁将在异常抛到同步方法边界之外时自动释放。

字节码例子说明同步指令:

void onlyMe(Foo f){
	synchronized(f){ 
		doSomething(); 
	} 
}

字节码指令部分如下:

注意:为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理程序,这个异常处理程序声明可处理所有的异常,它的目的就是用来执行monitorexit指令。

posted @ 2022-11-10 23:55  拿了桔子跑-范德依彪  阅读(231)  评论(0编辑  收藏  举报