JVM系列之Class文件结构、字节码指令集

1、Class文件结构

Java语言:跨平台的语言(write once,run anywhere),当Java源代码成功编译成字节码后,如果想在不同的平台上面运行, 则无须再次编译,这个优势不再那么吸引人了。Python、PHP、Perl、Ruby、Lisp等有强大的解释器,跨平台似乎已经快成为一门语言必选的特性

Java虚拟机:跨语言的平台
Java虚拟机不和包括Java在内的任何语言绑定,它只与.class文件这种特定的二进制文件格式所关联,无论使用何种语言进行软件开发, 只要能将源文件编译为正确的.class文件,那么这种语言就可以在Java虚拟机上执行,可以说统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁,所有的JVM全部遵守Java虚拟机规范,也就是说所有的JVM环境都是一样的, 这样一来字节码文件可以在各种JVM上进行。

在这里插入图片描述

想要让一个Java程序正确地运行在JVM中,Java源码就是必须要被编译为符合JVM规范的字节码。

  • 前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件,javac是一种能够将Java源码编译为字节码的前端编译器
  • javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法分析、语法分析、语义分析以及生成字节码

1.1、字节码文件里是什么

源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种十六进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成本地机器码

1.1.2、什么是字节码指令(byte code)

Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的操作码 (opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成,虚拟机中许多指令并不包含操作数,只有一个操作码,由于指令只有一个字节大小,所以最多只有256个,对于添加新的指令要非常小心,超过256个可就完蛋了

1.1.3、如何解读供虚拟机解释执行的十六进制字节码
  1. 使用Notepad++查看,安装HEX-Editor插件后进行查看
  2. 使用javap指令,JDK自带的反解析工具
  3. 使用IDEA插件,jclasslib或jclasslib bytecode viewer客户端工具

1.2、Class文件结构

1.2.1、Class类的本质

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说Class文件实际上它并不一定以磁盘文件形式存在。Class文件是一组以字节为基础单位的二进制流,在内存中的表现形式为字节数组

1.2.2、Class文件格式
  • Class的结构不像XML等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的。哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变
  • Class文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表
  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明
    在这里插入图片描述
1.2.3、Class文件结构概述
1.2.3.1、魔数(Class文件的标志)
  • Magic Number(魔数),每个Class文件开头的4个字节的无符号整数称为魔数(Magic Number)
  • 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符
  • 魔数值固定为0xCA FE BA BE,不会改变
1.2.3.2、Class文件版本号
  • 紧接着魔数的4个字节存储的是Class文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version
  • 它们共同构成了Class文件的格式版本号。譬如某个Class文件的主版本号为M,副版本号为m,那么这个Class文件的格式版本号就确定为M.m
    在这里插入图片描述
    版本号和Java编译器的对应关系如下表:
  • Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1,不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常(向下兼容)
  • 在实际应用中,由于开发环境和生产环境的不同,可能会导致该问题的发生。因此,需要我们在开发时,特别注意开发编译的JDK版本和生产环境的JDK版本是否一致
  • 虚拟机JDK版本为1.k (k >= 2)时,对应的Class文件格式版本号的范围为45.0 - 44 + k.0(含两端)
1.2.3.3、常量池(存放所有常量)

常量池是Class文件中内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用,随着Java虚拟机的不断发展,常量池的内容也日渐丰富,可以说常量池是整个Class文件的基石,在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。

  • 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count),与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的
    1. 通常我们写代码时都是从0开始的,但是这里的常量池却是从1开始,因为 它把第0项常量空出来了
    2. 这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项目"的含义,这种情况可用索引值0来表示
    3. 例如后面的父类索引,Object类没有父类,所以它的父类索引在常量池中就是0
  • 常量池表是一种表结构,索引为1到(constant_pool_count - 1)
  • 常量池中主要存放两大常量:字面量和符号引用
    1. 字面量:使用""引起来的字符串、使用final修饰的基本数据类型变量
    2. 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
  • 常量项大概有14种,结构都是标记字节(1字节) + 其他。比较常见的有CONSTANT_utf8_info、CONSTANT_class_info、CONSTANT_Methodref_info、CONSTANT_Fieldref_info等
    在这里插入图片描述
  • 常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一
1.2.3.4、访问标识

在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息

  • 这个Class是类还是接口
  • 是否定义为public类型
  • 是否定义为abstract类型
  • 如果是类的话,是否被声明为final等

在这里插入图片描述

1.2.3.5、类索引(this_class)、父类索引(super_class)、接口索引集合(interfaces_count、interfaces[])

这三项数据来确定这个类的继承关系以及实现的接口,就是用来描述类的全限定名、父类是谁、实现了多个个接口,具体是哪些接口
在这里插入图片描述

  • 类索引(this_class):类索引用于确定这个类的全限定名,2字节无符号整数,指向常量池的索引,常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个Class文件所定义的类或接口
  • 父类索引(super_class): 2字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名,如果我们没有继承任何类,其默认继承的是java/lang/Object 类。同时,由 于Java不支持多继承,所以其父类只有一个
  • 接口索引集合(interfaces_count、interfaces[]):
    1. interfaces_count项的值表示当前类或接口的直接超接口数量
    2. interfaces[]中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为interfaces_count
      1. 每个成员interfaces[i]必须为CONSTANT_Class_info结构,其中0 <= i < interfaces_count
      2. 在interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即 interfaces[0]对应的是源代码中最左边的接口
1.2.3.6、字段表集合
1.2.3.6.1、概述
  • 字段表集合包含字段计数器以及字段表
  • 字段表:用于描述接口或类中声明的变量
  • 字段(field)包括类级变量以及实例级变量, 但是不包括方法内部、代码块内部声明的局部变量
  • 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述
  • 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final 修饰符)等
  • 字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段
  • 在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管 是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个 字段的描述符不一致,那字段重名就是合法的
1.2.3.6.2、字段计数器

fields_count的值表示当前Class文件fields表的成员个数。使用两个字节来表示

1.2.3.6.2、字段表

fields表中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完整描述
在这里插入图片描述

  • 字段访问标志(access_flags)
    在这里插入图片描述
  • 字段名索引(name_index):根据字段名索引的值,查询常量池中的指定索引项即可
  • 描述符索引:字段的数据类型(基本数据类型、引用数据类型和数组)
    在这里插入图片描述
  • 属性表集合(属性个数和属性表数组)
    1. 一个字段还可能拥有一些属性,用于存储更多的额外信息。比如初始化值、 一些注释信息等
    2. 属性个数存放在attribute_count中
    3. 属性具体内容存放在attributes数组中
    4. 以常量属性为例,结构为:
      在这里插入图片描述
      在这里插入图片描述
1.2.3.7、方法表集合
  • 分为方法表计数器以及方法表数组
  • 方法表计数器:有多少个方法表
  • 方法表:用于表示当前类或接口中某个方法的完整描述
    在这里插入图片描述
    在这里插入图片描述
  • 方法名索引:对应常量池中CONSTANT_Uft8_info方法的名称
  • 描述符索引:对应常量池中符号引用方法的返回值和参数列表
  • 属性计数器:有多少个属性
  • 属性表:和前面字段表中的属性表类似,后面详细解释
1.2.3.8、属性表集合

在这里插入图片描述

  • 属性名索引:在常量池中的索引,其实引用的字符串常量
  • 属性长度:有多少个字节,便于校验

Java8中定义了23中属性表,如下图所示
在这里插入图片描述

  • 常见的Code属性表

Code属性就是存放方法体里面的代码,但是并非所有方法表都有Code属性,像接口或者抽象方法,他们没有具体的方法体,因此也就不会有Code属性了
在这里插入图片描述

  • 常见的LineNumberTable属性表

    1. LineNumberTable属性是可选变长属性,位于Code结构的属性表
    2. LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系,这个属性可以用来在调试的时候定位代码执行的行数
      在这里插入图片描述
  • 常见的LocalVariableTable属性表

    1. LocalVariableTable是可选变长属性,位于Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息
    2. LocalVariableTable属性表结构
      在这里插入图片描述
    3. attribute_name_index:属性表名称常量池表索引
    4. attribute_length:属性表长度
    5. local_variable_table_length:局部变量个数
      1. start_pc + length:这个变量在方法内的作用于
      2. name_index:变量名在常量池表的索引
      3. descriptor_index:局部变量数据类型在常量池表的索引
      4. index:变量在局部变量表中的槽位
1.2.3.9、总结

Class文件其实就是对类的整体描述,类有哪些字段?有哪些方法?类的全限定名?类的父类是谁?类实现的接口有哪些?方法中具体的方法体是什么?类的访问权限和修饰符等,Class文件中有一块非常重要的内容就是常量池,通过上面的分析可以得知,常量池中存储的符号引用,其他描述的地方引用常量池中的符号引用,最后都定位到字面量(字符串、基本数据类型)。

1.2.4、解析举例
  • 原始字节码文件
cafe babe 0000 0034 0016 0a00 0400 1209
0003 0013 0700 1407 0015 0100 036e 756d
0100 0149 0100 063c 696e 6974 3e01 0003
2829 5601 0004 436f 6465 0100 0f4c 696e
654e 756d 6265 7254 6162 6c65 0100 124c
6f63 616c 5661 7269 6162 6c65 5461 626c
6501 0004 7468 6973 0100 184c 636f 6d2f
6174 6775 6967 752f 6a61 7661 312f 4465
6d6f 3b01 0003 6164 6401 0003 2829 4901
000a 536f 7572 6365 4669 6c65 0100 0944
656d 6f2e 6a61 7661 0c00 0700 080c 0005
0006 0100 1663 6f6d 2f61 7467 7569 6775
2f6a 6176 6131 2f44 656d 6f01 0010 6a61
7661 2f6c 616e 672f 4f62 6a65 6374 0021
0003 0004 0000 0001 0002 0005 0006 0000
0002 0001 0007 0008 0001 0009 0000 0038
0002 0001 0000 000a 2ab7 0001 2a04 b500
02b1 0000 0002 000a 0000 000a 0002 0000
0007 0004 0008 000b 0000 000c 0001 0000
000a 000c 000d 0000 0001 000e 000f 0001
0009 0000 003d 0003 0001 0000 000f 2a2a
b400 0205 60b5 0002 2ab4 0002 ac00 0000
0200 0a00 0000 0a00 0200 0000 0b00 0a00
0c00 0b00 0000 0c00 0100 0000 0f00 0c00
0d00 0000 0100 1000 0000 0200 11
  • 源码

在这里插入图片描述

  • Demo.class解析图解
    在这里插入图片描述

2、字节码指令集

2.1、概述

字节码指令对于虚拟机,就好像汇编语言对于计算机,属于基本执行命令,字节码指令由一个字节长度的,代表着某种特定操作含义的数字(称为操作码:Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数:Operands)构成,由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码,由于限制了操作码的长度为一个字节(即0 ~ 255),这意味着指令集的操作码总数不可能超过256条 。
官方文档:

2.2、执行模型

如果不考虑异常处理的话,那么Java虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型来理解
在这里插入图片描述

2.3、字节码与数据类型

在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息,例如,iload指令用于从局部变量表中加载int类型的数据到操作数栈中,而fload指令加载的则是float类型的数据
在这里插入图片描述
对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型,还有一些指令,比如无条件跳转指令goto则是与数据类型无关。

2.4、指令的分类

  • 加载与存储指令:加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递
  • 算术指令:算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈
  • 类型转换指令:将两种不同的数值类型进行相互转换
  • 对象的创建与访问指令:创建对象、访问字段、数组操作和类型检查
  • 方法调用与返回指令:进行方法调用,结束方法以及放回相应的值
  • 操作数栈管理指令:用于操作操作数栈
  • 控制转移指令:进行流程控制例如if、if…else、if…else if…else、switch、for、while等
  • 异常处理指令:例如对于throw等关键字的处理
  • 同步控制指令:使用了synchronized关键字的处理

2.5、加载与存储指令

2.5.1、作用

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递

2.5.2、常用指令
2.5.2.1、局部变量压栈指令

将局部变量表中对应索引位置的变量压入操作数栈栈顶

指令格式:

  • xload index(其中x 为 i、l、f、d、a,其中index为局变量表索引值)
  • xload_ (其中x 为 i、l、f、d、a,n为0到3)
  • x为数据类型,n表示局部变量表索引的位置
  • 当局部变量表索引大于3时,就使用xload index指令
2.5.2.2、常量入栈指令
  • 将一个常量加载到操作数栈,分为const系列、push系列和ldc系列
  • const指令系列:用于对特定的常量压入操作数栈,入栈的常量隐含在指令本身里
    1. iconst_(第二个i从-1到5)
    2. lconst_(第二个l从0到1)
    3. fconst_(第二个f从0到2)
    4. dconst_(第二个d从0到1)
    5. aconst_null
    6. 从指令的命名上不难找出规律,指令助记符的第一个字符总是喜欢表示数据类型,i表示整数、l表示长整型、f表示浮点数、d表示双精度浮点,习惯上用a表示对象引用,如果指令隐含操作的参数,会以下划线形式给出。
  • push指令系列
    1. 主要包括bipush和sipush,它们的区别在于接受数据类型的不同
    2. bipush接收8位整数作为参数,将参数压入栈
    3. sipush接收16位整数,将参数压入栈
  • ldc指令系列
    1. 如果以上指令都不能满足需求,那么可以使用万能的ldc指令
    2. 它可以接收一个8位的参数,该参数指向常量池中的int、float或者String的索引,将指定的字面量压入操作数栈栈顶
  • ldc_w指令系列:它接收两个8位参数,能支持的索引范围大于ldc如果要压入的元素是long或者double类型的,则使用ldc2_w指令,使用方式都是类似的
2.5.2.3、出栈装入局部变量表指令

弹出操作数栈栈顶元素,存储到局部变量表对应索引处,给局部变量赋值

  • xstore index(x 为i、l、f、d、a,index为局部变量表索引值),指令xstore由于没有隐含参数信息,故需要提供一个byte类型的参数类指定目标局部变量表的位置
  • xstore_(其中x为i、l、f、d、a,n为0到3),弹出操作数栈对应栈顶元素,为局部变量表对应索引处的局部变量进行赋值
  • xastore(x 为i、l、f、d、a、b、c、s),专门针对数组操作,给数组索引处进行赋值,以iastore为例,它用于给一个int数组的给定索引赋值。在iastore执行前,操作数栈顶需要以此准备3个元素:值、索引、数组引用,iastore会弹出这3个值,并将值赋给数组中指定索引的位置
    在这里插入图片描述
2.5.3、总结

在这里插入图片描述

2.6、算术指令

2.6.1、作用

算术指令用于两个操作数栈上的值进行某种特定运算,并且把计算后的结果重新压入操作数栈

2.6.2、byte、short、char和boolean类型说明

Java虚拟机中没有直接支持byte、short、char和boolean类型的算术指令,而是使用int类型的指令来代替处理,在处理这些类型的数组时,也会转换成对应的int类型的字节码指令来处理。

Java虚拟机中的实际类型与运算类型
在这里插入图片描述

2.6.3、运算时溢出

数据运算可能会导致溢出,例如两个很大的正整数相加,结果可能是一个负数(补码),其实Java虚拟机规范并无明确规定过整型数据溢出的具体结果,仅规定了在处理整型数据时,只有除法指令以及求余指令中当出现除数为0时会导致虚拟机抛出异常ArithmeticException

2.6.4、分类
  • 加法指令
    1. iadd
    2. ladd
    3. fadd
    4. dadd
  • 减法指令
    1. isub
    2. lsub
    3. fsub
    4. dsub
  • 乘法指令
    1. imul
    2. lmul
    3. fmul
    4. dmul
  • 除法指令
    1. idiv
    2. ldiv
    3. fdiv
    4. ddiv
  • 求余指令
    1. irem
    2. lrem
    3. frem
    4. drem
  • 取反指令
    1. ineg
    2. lneg
    3. fneg
    4. dneg
  • 自增指令:iinc
  • 位运算指令
    1. 位移指令
      1. ishl
      2. ishr
      3. iushr
      4. lshl
      5. lshr
      6. lushr
    2. 按位或指令
      1. ior
      2. lor
    3. 按位与指令
      1. iand
      2. land
    4. 按位异或指令
      1. ixor
      2. lxor
  • 比较指令
    1. dcmpg
    2. dcmlp
    3. fcmpg
    4. fcmpl
    5. lcmp
    6. 数值类型的数据才可以谈大小,boolean、引用数据类型不能比较大小
2.6.5、举例

在这里插入图片描述
在这里插入图片描述

2.7、类型转换指令

2.7.1、说明

类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题

2.7.2、类型转换分成两种,宽化类型转换和窄化类型转换
2.7.2.1、宽化类型转换
  • 转换规则:Java虚拟机直接支持以下数值的宽化类型转换(Widening Numeric Conversion,小范围类型向大范围类型的安全转换),也就是说,并不需要执行字节码指令,包括:
    1. 从int类型到long、float或者double类型,对应的指令为:i2l、i2f、i2d
    2. 从long类型到float、double类型。对应的指令为:l2f、l2d
    3. 从flaot类型到double类型。对应的指令为:f2d
    4. 简化为:int -> long -> float -> double
  • 精度丢失问题:
    1. 从byte、char和short类型到int类型的宽化类型转换实际上是不存在的,对于byte类型转换为int,虚拟机并没有处理。而byte转为long 时,使用的是i2l
    2. int转换到long,或者从int转换到double,都不会丢失任何信息,转换前后的值是精确相等的
    3. 从int、long类型数值转换到float,或者long类型树脂转换到double时,将可能发生丢失精度
2.7.2.2、窄化类型转换
  • 转换规则
    1. 从int类型至byte、short或者char类型。对应的指令有:i2b、i2c、i2s
    2. 从long类型到int类型。对应的指令有:l2i
    3. 从float类型到int或者long类型。对应的指令有:f2i、f2l
    4. 从double类型到int、long或者float类型。对应的指令有:d2i、d2l、d2f
  • 精度丢失问题
    1. 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,因此, 转换过程很可能会导致数值丢失精度
    2. 尽管数据类型窄化转换可能会发生上限溢出、下限溢出和精度丢失等情况
    3. 但是Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常

2.8、对象的创建与访问指令

2.8.1、前言

Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持,有一系列指令专门用于对象操作,可进一步细分为创建指令、 字段访问指令、数组操作指令和类型检查指令

2.8.2、创建指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令

  • 创建类实例的指令
    1. new
    2. 它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后, 将对象的引用压入栈
  • 创建数组的指令
    1. newarray :创建基本类型数组
    2. anewarray: 创建引用类型数组
    3. multianewarray:创建多维数组

上述创建指令可以用于创建对象或者数组,由于对象和数组在Java中的广泛使用,这些指令的使用频率也很高

2.8.3、字段访问指令
  • 对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素
  • 访问类字段(static字段,或者称为类变量)的指令:getstatic、putstatic
  • 访问类实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield
2.8.4、数组操作指令

数组操作指令主要有:xastore和xaload指令,其中x为具体的数据类型

xaload指令:

  • 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、 laload、faload、daload、aaload
  • 指令xaload表示将数组的元素压栈,比如saload、caload分别表示压入short数组和char数组
  • 指令xaload在执行时,要求操作数中栈顶元素为数组索引i,栈顶顺位第2个元素为数组引用a,该指令会弹出栈顶这两个元素,并将a[i]重新压入堆栈

将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、 iastore、lastore、fastore、dastore、aastore
在这里插入图片描述

  • 取数组长度的指令:arraylength,该指令弹出栈顶的数组元素,获取数组的长度,将长度压入栈
2.8.5、类型检查指令
  • 检查类实例或数组类型的指令:instanceof、checkcast
  • 指令checkcast用于检查类型强制转换是否可以进行,如果可以进行,那么checkcast指令不会改变操作数栈,否则它会抛出ClassCastException异常
  • 指令instanceof用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈

2.9、方法调用与返回指令

2.9.1、方法调用指令

方法调用指令主要有5个:invokevirtual、invokeinterface、invokespecial、invokestatic、invokedynamic

  • invokevirtual:指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态,这也是Java语言中最常见的方法分派方式
  • invokeinterface:指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用
  • invokespecial:指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法,这些方法都是静态类型绑定的,不会在调用时进行动态派发
  • invokestatic:指令用于调用命名类中的类方法(static方法)。这是静态绑定的
  • invokedynamic:调用动态绑定的方法,这个是JDK1.7后新加入的指令,用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的
2.9.2、返回指令

方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的,包含xreturn和return指令。

  • xreturn指令
    1. xreturn返回指令需要返回数据,x表示具体的数据类型
    2. ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn
  • return指令
    1. 实例方法返回void,无返回值
    2. 实例初始化方法
    3. 类和接口的类初始化方法使用

在这里插入图片描述

通过ireturn指令,将当前函数操作数栈的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃,如果当前返回的是synchronized方法,那么还会执行一 个隐含的monitorexit指令,退出临界区,最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者。

2.10、操作数栈管理指令

2.10.1、前言

如同操作一个普通数据结构中的堆栈那样,JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令

2.10.2、pop、pop2

将一个或两个元素从栈顶弹出,并且直接废弃

  • pop:将栈顶的1个Slot数值出栈。例如1个short类型数值
  • pop2:将栈顶的2个Slot数值出栈。例如1个double类型数值,或者2个int类型数值
2.10.3、dup、dup2、dup_x1、dup2_x1、du p_x2、dup2_x2

复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶

2.10.4、swap

将栈最顶端的两个Slot数值位置交换:swap,Java虚拟机没有提供交换两个64位数据类型(long、double)数值的指令

2.10.5、nop

指令nop是一个非常特殊的指令,它的字节码为0x00,和汇编语言中的nop一样,它表示什么都不做,这条指令一般可用于调试、占位等

2.11、控制转移指令

程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为比较指令、条件跳转指令、比较条件跳转指令、多条件分支跳转指令、无条件跳转指令等

2.12、异常处理指令

2.12.1、抛出异常指令
  • athrow指令
    1. 在Java程序中显式抛出异常的操作(throw语句)都是由athrow指令来实现的
    2. 除了使用throw语句显式抛出异常情况之外,JVM规范还规定了许多运行时异常会在其它Java虚拟机指令检测到异常状况时自动抛出
    3. 例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常
      注意:正常情况下,操作数栈的压入弹出都是一条条指令完成的,唯一的例外情况是在抛异常时,Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上
2.12.2、异常的处理
  • 过程一:异常对象的生成过程 -> throw(手动/自动) -> 指令:athrow
  • 过程二:异常的处理:抓抛模型try - catch - finally -> 使用异常表
2.12.3、处理异常与异常表
  • 处理异常:在Java虚拟机中,处理异常(catch 语句)不是由字节码指令来实现的(早期使用jsr、ret指令),而是采用异常表来完成的
  • 异常表
    1. 如果一个方法定义了一个try - catch或者try - finally的异常处理,就会创建 一个异常表
    2. 异常表包含了每个异常处理或者finally块的信息
    3. 异常表保存了每个异常处理信息
      1. 起始位置
      2. 结束位置
      3. 程序计数器记录的代码处理的偏移地址
      4. 被捕获的异常类在常量池中的索引
  • 详细处理过程
    1. 当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理
    2. 如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)
    3. 如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止
    4. 如果这个异常在最后一个非守护线程里抛出,将会导致JV 自己终止,比如这个线程是个main线程
    5. 不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行
    6. 在这种情况下, 如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标

2.13、同步控制指令

Java虚拟机支持两种同步结构:方法级同步和方法内部一段指令序列的同步,这两种同步都是使用monitor(本质上是对象中的对象头支持的)来支持的

2.13.1、方法级的同步

方法级的同步:是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中

虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法,当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,执行线程将先持有同步锁,然后执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁,在方法执行期间,执行线程持有了同步锁,其它任何线程都无法再获得同一个锁(底层依赖操作系统的互斥锁mutex),如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常, 那么这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。

在这里插入图片描述
在这里插入图片描述
这段代码和普通的无同步操作的代码没有什么不同,没有使用monitorenter和monitorexit进行同步区控制,这是因为,对于同步方法而言,当虚拟机通过方法的访问标识符判断是一个同步方法时,会自动在方法调用前进行加锁,当同 步方法执行完毕后,不管方法是正常结束还是有异常抛出,均会由虚拟机释放这个锁,因此,对于同步方法而言,monitorenter和monitorexit指令是隐式存在的,并未直接出现在字节码中。

2.13.2、方法内指定指令序列的同步
  • 同步一段指令集序列,通常是由Java中的synchronized代码块来表示的,JVM的指令集有monitorenter和monitorexit两条指令来支持synchronized关键字的语义
  • 执行流程:
    1. 当一个线程进入同步代码块时,它使用monitorenter指令请求进入
    2. 如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入(可重入性),否则进行等待,知道对象的监视器计数器为0,才会被允许进入同步块当线程退出同步块时,需要使用monitorexit声明退出
    3. 在Java虚拟机中, 任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态指令monitorenter和monitorexit在执行时,都需要在操作数栈顶压入对象,之后monitorenter和monitorexit的锁定和释放都是针对这个对象的监视器进行的
  • 锁必须要释放
    1. 编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须执行其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束
    2. 为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit指令
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值