《深入理解Java虚拟机》第6章:类文件结构

6. 类文件结构

在这里插入图片描述

任何一个Class文件都对应着唯一的一个类或接口的定义信息,但类或接口并不一定都得定义在文件里,譬如类或接口也可以动态生成,直接送入类加载器中

Class文件的存储方式

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符

当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储

  • Big-Endian:高位在前。高位字节在地址最低位,最低字节在地址最高位来存储,SPARC、PowerPC等处理器的默认多字节存储顺序
  • Little-Endian:高位字节在地址最高位,最低字节在地址最低位来存储,x86等处理器的默认存储方式

Class文件的两种数据类型

  • 无符号数:基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
  • :由多个无符号数或者其他表作为数据项构成的复合数据类型,习惯性地以“_info”结尾。整个Class文件本质上也可以看作是一张表

6.3 Class类文件的结构

Class文件格式

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器(如fields_count)加若干个连续的数据项(如fields[fields_count])的形式

这个格式是固定的,且没有分隔符的!

Class文件展示

  • java源码(JDK 9版本)

    public class Test {
        public static void main(String[] args) {
            System.out.println("yes");
        }
    }
    
  • Class文件的16进制文本
    在这里插入图片描述

魔数与Class文件的版本

u4             magic;
u2             minor_version;
u2             major_version;

魔数
每个Class文件的头4个字节被称为魔数(Magic Number)
它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件
使用魔数比使用拓展名安全,后者可以更改

不仅是Class文件,很多文件格式标准都有使用魔数进行身份识别,譬如图片格式

版本号

  • 第5和第6个字节是次版本号(Minor Version),次版本号很长时间不使用了,从JDK 1.2以后,直到JDK 12都是0x0000
  • 第7和第8个字节是主版本号(Major Version)

Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),JDK 13的主版本号是57
高版本可以兼容低版本,但即便文件格式未变化,也规定了低版本不可以运行高版本的Class文件
低版本运行高版本,将抛出异常:java.lang.UnsupportedClassVersionError

示例:Test.class

在这里插入图片描述

上图是JDK 9版本的一个Class文件
前4个字节为魔数,0xCAFEBABE表示是java class文件
次版本号为 0x0000,主版本号为0x0035,即53(版本9)

常量池

u2             constant_pool_count;
cp_info        constant_pool[constant_pool_count-1];

占用Class文件空间最大的数据项目之一,也是Class文件中第一个出现的表类型数据项目

常量池中常量的数量是不固定的,所以需要先设置一个常量池容量计数值(constant_pool_count)
常量池容量计数是从1而不是0开始:如常量池容量计数值是 0x0022,即十进制的34,这就代表有33项常量,索引值范围为1~33
第0项常量:特殊作用,对于那些规定必须指向常量池的数据项,但实际上又用不着常量池时,可以使他们指向第0项
除了常量池容量计数值是从1开始的以外,其他的容量计数都是从0开始

常量池中的两大类常量

  • 字面量:如文本字符串、被声明为final的常量值
  • 符号引用:编译原理方面的概念,包括:
    • 被模块导出或者开放的包(Package)
    • 类和接口的全限定名(Fully Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符
    • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
    • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

Java编译没有“连接”,而是在虚拟机加载Class文件时进行动态连接。编译生成的Class文件中的符号引用,并不能代表内存地址

常量表
常量池中每一项常量都是一个表
最初常量表中共有11种结构各不相同的表结构数据,后来增至17种(截至JDK 13)

  • tag字段:常量表的第一个字段是tag,u1类型的标志位,代表着当前常量属于哪种常量类型
类型(17种)标志(tag)描述
CONSTANT_Utf8_info1UTF-8编码的字符串
CONSTANT_Integer_info3整型字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整型字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类或接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref _info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12字段或方法的部分符号引用
CONSTANT_MethodHandle_info15表示方法句柄
CONSTANT_MethodType_info16表示方法类型
CONSTANT_ Dynamic_info17表示一个动态计算常量
CONSTANT_InvokeDynanmic_info18表示一个动态方法调用点
CONSTANT_Module_info19表示一个模块
CONSTANT_Package_info20表示一个模块中开放或者导出的包
  • 17种数据类型的结构总表
常量项目类型描述
CONSTANT_Utf8_infotagu1值为1
lengthu2UTF-8编码的字符串占用了字节数
bytesu1长度为length的UTF-8编码的字符串
CONSTANT_Integer_infotagu1值为3
bytesu4按照高位在前存储的int值
CONSTANT_Float_infotagu1值为4
bytesu4按照高位在前存储的float值
CONSTANT_Long_infotagu1值为5
bytesu8按照高位在前存储的long值
CONSTANT_Double_infotagu1值为6
bytesu8按照高位在前存储的double值
CONSTANT_Class_infotagu1值为7
indexu2指向全限定名常量项的索引
CONSTANT_String_infotagu1值为8
indexu2指向字符串字面量的索引
CONSTANT_Fieldref_infotagu1值为9
indexu2指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
indexu2指向字段描述符CONSTANT_NameAndType的索引项
CONSTANT_Methodref_infotagu1值为10
indexu2指向声明方法的类描述符CONSTANT_Class_info的索引项
indexu2指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_InterfaceMethodref_infotagu1值为11
indexu2指向声明方法的接口描述符CONSTANT_Class_info的索引项
indexu2指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_NameAndType_infotagu1值为12
indexu2指向该字段或方法名称常量项的索引
indexu2指向该字段或方法描述符常量项的索引
CONSTANT_MethodHandle_infotagu1值为15
reference_kindu1值必须在1至9之间(包括1和 9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_indexu2值必须是对常量池的有效索引
CONSTANT_MethodType_infotagu1值为16
descriptor_indexu2值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符
CONSTANT_Dynamic_infotagu1值为17
bootstrap_method_attr_indexu2值必须是对当前 Class文件中引导方法表的 bootstrap_methods[]数组的有效索引
name_and_type_indexu2值必须是对当前常量池的有效索引,常量池在该索引处的项是CONSTANT_NameAndType_info结构,表示方法名和方法描述符
CONSTANT_InvokeDynamic_infotagu1值为18
bootstrap_method_attr_indexu2值必须是对当前 Class文件中引导方法表的 bootstrap_methods[]数组的有效索引
name_and_type_indexu2值必须是对当前常量池的有效索引,常量池在该索引处的项是CONSTANT_NamcAndType_info结构,表示方法名和方法描述符
CONSTANT_Module_infotagu1值为19
name_indexu2值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示模块名字
CONSTANT_Package_infotagu1值为20
name_indexu2值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示包名称
  • 示例:Test.class

在这里插入图片描述

(1)可以看到第一项常量是0x0A,即10,表示的是CONSTANT_Methodref _info,类中方法的符号引用
(2)CONSTANT_Methodref _info后还有两个u2类型的index字段,也就是0x0A后面的4个字段都属于它
(3)第二项常量从第一行的最后一个开始,0x09表示这是一个CONSTANT_Fieldref_info,同样有两个u2类型的index字段
(4)第三项常量从第二行的第5个开始,0x08表示这是CONSTANT_String_info,它后面跟一个u2
(5)第四项常量从第二行的第8个开始……如此,直到所有常量标识完毕


访问标志

u2             access_flags;

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags)

标志名称标志值含义
ACC_PUBLIC0x0001是否为 public类型
ACC_FINAL0x0010是否被声明为final,只有类可设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义,
invokespecial指令在JDK 1.0.2发生过改变
要求这之后该字段都为真
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举
ACC_MODULE0x8000标识这是一个模块

access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一律为零

ACC_SYNTHETIC当代码是由编译器生成,而不会出现在源码中时,为真

  • 示例:Test.class

    public class Test {
        public static void main(String[] args) {
            System.out.println("yes");
        }
    }
    //ACC_PUBLIC、ACC_SUPER为真
    //ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、ACC_MODULE为假
    //access_flags标志位就是:0x0001 | 0x0020 = 0x0021
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ncPKUS3E-1669456315777)(C:\Users\jiang\AppData\Roaming\Typora\typora-user-images\image-20221126134008112.png)]

类索引、父类索引与接口索引集合

u2             this_class;
u2             super_class;
u2             interfaces_count;
u2             interfaces[interfaces_count];

Class文件中由这三项数据来确定该类型的继承关系

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名
除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0

接口索引集合就用来描述这个类/接口implements/extends哪些接口,按从左到右的顺序罗列

类索引查找全限定名的过程
以this_class为例,this_class指向一个类型为CONSTANT_Class_info的类描述符常量
然后根据该常量值可以找到CONSTANT_Utf8_info类型的全限定名字符串

CONSTANT_Class_info和CONSTANT_Utf8_info都是在常量池中定义的
正是因为有这些信息,所以前面简单的Java代码就有30多个常量

在这里插入图片描述

  • 示例:Test.class

在这里插入图片描述

访问标志之后的三个u2是 this_class,super_class,interfaces_count,分别表示类索引为5,父类索引为6,接口索引集合大小为0;
通过反编译验证:

javap -v Test.java

在这里插入图片描述

可以看到常量池中的第5和第6项的确是本类名和父类名

字段表集合

u2             fields_count;
field_info     fields[fields_count];

字段内容
这里的字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量,包括:

  • 作用域修饰符:public private protected
  • 实例变量还是类变量:static修饰的为类变量
  • 是否为final修饰
  • 是否为violatile修饰:强迫每次访问该变量时都从主内存读写(即面临共享变量时,不能保存它的私有备份)
  • 是否为transient修饰:可否被序列化
  • 字段数据类型:基本类型、对象、数组
  • 字段名称

字段表结构
在这里插入图片描述

(1)access_flag:字段访问标志,一个u2类型
在这里插入图片描述

(2)name_index:字段的简单名称
例如,方法func()和变量var,它们的简单名称分别是func和var

(3)descriptor_index:字段和方法的描述符
描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值
在这里插入图片描述

描述符规则:

  • 基本数据类型及void:用一个大写字母标识
  • 对象类型:字符L加对象的全限定名,这时一般会加“;”用于隔开
  • 数组类型:n维数组就在前面加上n个“[”;如“java.lang.String [] []”就是“[[Ljava/lang/String;”。一个整型数组“int[]”就是“[I”

描述方法时,按照先参数列表,再返回值的顺序,参数列表写在()之中

void inc()		//()V
java.lang.String toString()		//()Ljava/lang/String;
int indexOf(char[]source, int sourceOffset, int sourceCount,char[]target, int targetOffset, int targetCount, int fromIndex)		
//([CII[CIII)I

(4)attributes_count和attributes:额外描述的信息,常为0x00,不怎么用

  • 示例:Test.class和Test2.class

    在这里插入图片描述

    由于测试代码里面没有定义变量,可以看到这个Class文件里紧接着代表fields_count的值为0,表示字段表结束了
    为了更好地测试,这里用下列代码生成了一份含有字段表的Class文件

    //测试字段表
    public class Test2 {
        private int m;
        public int inc(){
            return m+1;
        }
    }
    

    对应的Class文件片段:

    在这里插入图片描述

    依然是0x0021为访问标志,后面跟3个u2数据分别表示类索引、父类索引和空的接口索引,第4个u2数据就是字段表的开始

    • 由于增加了一个m字段,此时可以看到fields_count=1

    • access_flag=2,表示private

    • name_index=5,通过javap -v Test2.class,查看新的常量池,可以看到第5项是m

      在这里插入图片描述

    • descriptor_index=6,这里表示字段描述符,可以看到I,及类型为int型

    这里后面还跟了一个0x0000,即attributes_count=0,这个u2过了之后,才是下一个部分

  • 总结
    字段表的内容,就是首先用一个fields_count说明有多少变量,后面跟fields_count个(修饰符、变量名、变量类型)结构;(这里假设不存在额外信息)

  • 注意
    字段表中不包括继承而来的字段
    字段表中有可能出现非用户自身定义的字段,例如编译器有时会添加一些指向外部实例的字段
    字段不能重载(语言规定),哪怕它们拥有不同的修饰符和数据类型;但Class文件不会发现这个问题

方法表集合

u2             methods_count;
method_info    methods[methods_count];

方法表结构
在这里插入图片描述

和字段表结构相似,只是access_flags有部分不同,以及属性表集合的可选项有不同

方法访问标志
在这里插入图片描述

ACC_VOLATILE标志和ACC_TRANSIENT标志不能修饰方法,因此被去掉了
增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志

方法依然由(修饰符、方法名、参数和返回类型)三个字段以及额外信息来存储,和字段不太一样的地方是方法会用 () 区分参数类型和返回类型

示例:Test.class
在这里插入图片描述

  • 0x0002:表示methods_counts=2,有2个方法

  • (0x0001 0007 0008):描述第1个方法的基本信息,查常量池可以发现是(public init ())方法
    在这里插入图片描述

  • 0x0001:attributes_count=1,第1个方法有额外信息

  • 0x0009:表示attributs_name_index,它为9,查常量池发现表示是“code”,即此属性是方法的字节码描述
    在这里插入图片描述
    有关attributes的内容见下节

注意
(1)如果子类没有重写父类方法,那方法表中就不会出现该方法。但会出现编译器添加的方法,典型的如 init() 方法
(2)在Java中,要进行方法重载,就需要有不同的特征签名。这个特征签名就是一个方法中各个参数在常量池中的字段符号
引用的集合,返回值不包括在其中。这是java语句的规定,但只有返回值不同的方法在Class文件中是可以共存的(Class不会进行检测)
(3)方法表的代码放在了方法属性表集合中一个名为“Code”的属性里面



属性表集合

u2             attributes_count;
attribute_info attributes[attributes_count];

Class文件、字段表、方法表都可以携带自己的属性表集合

属性表结构
在这里插入图片描述

属性表的限制会宽松一点,根据不同属性值,有不同的具体结构,但都遵循如上的基本结构

  • attribute_name_index:对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示
  • attribute_length:属性值的结构则是完全自定义的,attribute_length说明属性值所占用的位数
  • info:具体属性值的内容,attribute_length已提前说明了它的长度

属性非常多,这里只重点介绍code属性,其他还有Exceptions属性、LineNumberTable属性等等

Code属性
在这里插入图片描述

  • attribute_name_index:指向CONSTANT_Utf8_info型常量的索引,这里常量值固定为“Code”

  • attribute_length:等于整个属性表长度 - 6

  • max_stack:操作数栈(Operand Stack)深度的最大值。虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度

  • max_locals:代表了局部变量表所需的存储空间,单位是变量槽(Slot)。方法内的变量都依赖局部变量表。
    操作数栈和局部变量表直接决定一个该方法的栈帧所耗费的内存,但max_locals并不等于所有局部变量耗费变量槽之和
    Java虚拟机将局部变量表中的变量槽进行重用:和作用域有关,小作用域的局部变量可以被覆盖
    Javac编译器根据同时生存的最大局部变量数量和类型计算出max_locals的大小

  • code_length和code:存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。虚拟机每读到一个code,就找出该code对应的指令
    u1是0~255的操作码,《Java虚拟机规范》目前已定义了其中约200条编码值对应的指令含义

    code_length理论上可以达到232长度,但Java只允许其使用u2的长度,即216,超过的话就报错
    大部分情况是够用的,但如果一个方法过于复杂,也有可能因为生成的字节码超长而编译出错

  • exception_table_length和exception_table:非必要存在,有的code属性表就没有
    在这里插入图片描述

    字段含义:如果当字节码从第start_pc行到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转到handler_pc处进行处理。

    《Java虚拟机规范》中明确要求Java语言的编译器应当选择使用异常表而不是通过跳转指令来实现Java异常及finally处理机制

  • attributes_count和attribute:属性表的属性表,一样的

code_length和code解析

  • 以Test.java为例

    public class Test {
        public static void main(String[] args) {
            System.out.println("yes");
        }
    }
    
  • 对应的字节码文件里,方法表中的属性表(接着上次的分析继续)
    在这里插入图片描述

    前面在方法表中提到这里最后的 0x0001 0009,表示init方法有1个属性表,这个属性表的attribute_name_index为0x0009,查常量为“Code”,接下来的

    • 0x0000:code属性表中的attribute_name_index,它指向0,表示不使用常量池

    • 0x002F:attribute_length,为47字节

    • (0x0001 0x0001 ):分别表示操作数栈、局部变量表都为1

    • 0x00000005:4字节的code_length,表示长度为5

    • 0x2AB70001B1:即5字节的code
      在这里插入图片描述

      对Class文件反编译,可以看到init的Code的部分

      • 关于args_size=1:这是因为实例方法有默认的this变量,如果是static修饰的方法,就没有this了。这也是局部变量表为1的原因
      • 这里索引为2和3的指令没有显示出来,原理尚不知

6.4 字节码指令简介

这一部分粗略了解,遇到再查询即可

Java虚拟机指令集

操作码和操作数

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)构成

Java虚拟机采用面向操作数栈而不是面向寄存器的架构,因此大多数指令只有一个操作码,操作数都放在操作数栈中
意味着指令集的操作码总数不能够超过256条

//Java虚拟机的解释器的工作示例
do {
    自动计算PC寄存器的值加1;
    根据PC寄存器指示的位置,从字节码流中取出操作码;
    if (字节码存在操作数) 从字节码流中取出操作数;
    执行操作码所定义的操作;
} while (字节码流长度 > 0);

Java虚拟机指令集

举例:iload表示从局部变量表中加载int型的数据到操作数栈中。fload则加载float类型
这是一个与数据类型相关的字节码指令,但如果对每一个指令都设置针对不同数据类型的版本,那操作码总数很快就会超过256;例如load指令针对8中数据类型就要有8个版本
在这里插入图片描述

上图是Java虚拟机指令集所支持的数据类型的部分示例
实际上,大部分指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型

加载和存储指令

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

说明:

iload_<n>,它代表了iload_0、iload_1、iload_2和iload_3这几条指令

运算指令

//加法指令:
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
//按位异或指令:
ixor、lxor
//局部变量自增指令:
iinc
//比较指令:
dcmpg、dcmpl、fcmpg、fcmpl、lcmp
  • 规定在处理整型数据时
    只有除法指令(idiv和ldiv)以及求余指令(irem和lrem)中当出现除数为零时会导致虚拟机抛出ArithmeticException异常,其余任何整型数运算场景都不应该抛出运行时异常。
  • 规定在处理浮点数据时,必须完全支持“非正规浮点数值”和“逐级下溢”的运算规则
    • 默认舍入模式:向最接近数舍入模式
      JVM要求在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的
    • 把浮点数转换为整数时:向零舍入模式
      数字截断,将在目标数值类型中选择一个最接近,但是不大于原值的数字来作为最精确的舍入结果
    • 不会抛出任何运行时异常
      用无穷大或NaN表示异常结果

类型转换指令

  • 宽化类型转换:转换时无须显式的转换指令

    int类型到long、float或者double类型
    long类型到float、double类型
    float类型到double类型
    
  • 窄化类型转换:转换时需要显式的转换指令

    i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f
    //窄化类型转换可能会导致正负错误、精度丢失等问题
    

窄化类型转换的情况:

  • long ——> int

    • 截断,取后32位
  • 浮点值 ——> int或long

    • 如果浮点值是NaN,那转换结果就是0
    • 浮点值非无穷大,向零舍入模式取整。
    • 浮点数是无穷大,转换为其能表示的最大或者最小正数
  • double ——> float

    • 向最接近数舍入模式舍入得到一个可以使用float类型表示的数字
    • 如果转换结果的绝对值太小、无法使用float来表示的话,将返回float类型的正负零
    • 如果转换结果的绝对值太大、无法使用float来表示的话,将返回float类型的正负无穷大

对象创建与访问指令

//创建类实例的指令:
new
//创建数组的指令:
newarray、anewarray、multianewarray
//访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:
getfield、putfield、getstatic、putstatic
//把一个数组元素加载到操作数栈的指令:
baload、caload、saload、iaload、laload、faload、daload、aaload
//将一个操作数栈的值储存到数组元素中的指令:
bastore、castore、sastore、iastore、fastore、dastore、aastore
//取数组长度的指令:
arraylength
//检查类实例类型的指令:
instanceof、checkcast

操作数栈管理指令

//将操作数栈的栈顶一个或两个元素出栈
pop、pop2
//复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶
dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
//将栈最顶端的两个数值互换
swap

控制转移指令

//条件分支:
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
  • Java虚拟机中有专门的指令集用来处理int和reference类型的条件分支比较操作,也有专门的指令检测null值
  • 对于boolean类型、byte类型、char类型和short类型的条件分支比较操作,都使用int类型的比较指令来完成
  • 对于long类型、float类型和double类型的条件分支比较操作,则会先执行相应类型的运算指令,得到一个返回的整型值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转

最终都是使用int进行条件分支的判断

方法调用和返回指令

  • invokevirtual指令

    用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。

  • invokeinterface指令

    用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用

  • invokespecial指令

    用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法

  • invokestatic指令

    用于调用类静态方法(static方法)

  • invokedynamic指令

    用于在运行时动态解析出调用点限定符所引用的方法
    前面四条调用指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,例如 ireturn

异常处理指令

  • 抛出异常
    • 显式抛出:如throw语句,由athrow指令来实现
    • 非显式:许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出
  • 处理异常
    • 而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成

同步指令

使用管程(Monitor,更常见的是直接将它称为“锁”)来实现的

  • 方法级的同步是隐式的,无须通过字节码指令来控制
  • 同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的。Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值