《深入理解Java虚拟机——JVM高级特性与最佳实践》学习笔记——Java类文件结构

 2019-12-22 11:23  阅读(1010)
文章分类:JVM

《深入理解Java虚拟机——JVM高级特性与最佳实践》学习笔记——Java类文件结构

1.概述

由于计算机只能识别0和1,所以我们编写的程序需经过编译器编译后(二进制格式)才能运行在计算机上,然而随着虚拟机的发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码

Java虚拟机不和包括Java在内的任何语言绑定,它只与”Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息

2.Class类文件的结构

Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础

无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以”_info”结尾。表用于描述有层次关系的复合结构的数据类型,整个Class文件本质上就是一张表,它由以下所示的数据项构成

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

2.1 魔数与Class文件的版本

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件,值为:0xCAFEBABE

第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件

2.2 常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用了Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目

常量池的入口放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count),这个容量计算是从1开始,有21项常量,索引值范围为1~21,如果需要表达”不引用任何一个常量池项目”,可以把索引值置为0

常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

Java代码在进行Javac编译的时候,并不像C和C++那样有”连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就是无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中

常量池中每一项常量都是一个表,在JDK 1.7之前共有11种结构各不相同的表结构数据,在JDK 1.7中为了更好地支持动态语言调用,又额外增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info),这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位,代表当前这个常量属于哪种常量类型。这14种常量类型所代表的具体含义如下表所示

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 标识方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

2.3 访问标志

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。具体标志位以及标志含义见表

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK1.0.2发生过改变,为了区别这条指令使用哪些语意,JDK1.0.2之后编译出来的类的这个标志都必须为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类值为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生的
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

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

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

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串

对于接口索引集合,入口的第一项——u2类型的数据为接口计数器,表示索引表的容量,如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节

2.5 字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段表的结构(通过引用常量池中的常量来描述字段信息)如下

类型 名称 数量 类型 名称 数量
u2 access_flags 1 u2 attributes_count 1
u2 name_index 1 attribute_info attributes attributes_count
u2 descriptor_index 1

字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型,其中可以设置的标志位和含义见下表

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否public
ACC_PRIVATE 0x0002 字段是否private
ACC_PROTECTED 0x0004 字段是否protected
ACC_STATIC 0x0008 字段是否static
ACC_FINAL 0x0010 字段是否final
ACC_VOLATILE 0x0040 字段是否volatile
ACC_TRANSIENT 0x0080 字段是否transient
ACC_SYNTHETIC 0x1000 字段是否有编译器自动产生的
ACC_ENUM 0x4000 字段是否enum

跟随着access_flags标志的是两项索引值:name_index和descriptor_index,它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符

简单名称是指没有类型和参数修饰的方法或者字段名称;描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,如下表

标识字符 含义 标识字符 含义
B 基本类型byte J 基本类型long
C 基本类型char S 基本类型short
D 基本类型double Z 基本类型boolean
F 基本类型float V 特殊类型void
I 基本类型int L 对象类型,如LJava/lang/Object

对于数组类型,每一维度将使用一个前置的”[“字符来描述,如一个定义为”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”

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的

2.6 方法表集合

Class文件存储格式对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包含了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,如下表

类型 名称 数量 类型 名称 数量
u2 access_flags 1 u2 attributes_count 1
u2 name_index 1 attribute_info attributes attributes_count
u2 descriptor_index 1

这些数据项目的含义也非常类似,仅在访问标志和属性表集合的可选项中有所区别。因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对的,synchronized、native、strictfp和abstract关键字可以修饰方法,所以方法表的访问标志中增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。如下表所示

标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否为public
ACC_PRIVATE 0x0002 方法是否为private
ACC_PROTECTED 0x0004 方法是否为protected
ACC_STATIC 0x0008 方法是否为static
ACC_FINAL 0x0010 方法是否为final
ACC_SYNCHRONIZED 0x0020 方法是否为synchronized
ACC_BRIDGE 0x0040 方法是否是由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICTFP 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 方法是否是由编译器自动产生的

方法的定义可以通过访问标志、名称索引、描述符索引表达清除,而方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为”Code”的属性里面

2.7 属性表集合

属性表(attribute_info)在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息

虚拟机规范预定义的属性如下表所示

属性名称 使用位置 含义
Code 方法表 Java代码
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 JDK1.6中新增的属性,供新的类型检查验证器(TypeChecker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature 类、方法表、字段表 JDK1.5中新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(TypeVariables)或参数化类型(ParameterizedTypes),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 JDK1.6中新增的属性,SourceDebugExtension属性用于存储额外的调试信息。譬如在进行JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension属性就可以用于存储这个标准所加入的调试信息
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成的
LocalVariableTypeTable JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations 类、方法表、字段表 JDK1.5中新增的属性,为动态注解提供支持,RuntimeVisibleAnnotations属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的
RuntimeInvisibleAnnotations 类、方法表、字段表 JDK1.5中新增的属性,与RuntimeVisibleAnnotations属性作用刚好相反,用于指明哪些注释是运行时不可见的
RuntimeVisibleParameterAnnotations 方法表 JDK1.5中新增的属性,作用于RuntimeVisibleAnnotations属性类似,只不过作用对象为方法参数
RuntimeInvisibleParameterAnnotations 方法表 JDK1.5中新增的属性,作用于RuntimeInvisibleAnnotations属性类似,只不过作用对象为方法参数
AnnotationDefault 方法表 JDK1.5中新增的属性,用于记录注解类元素的默认值
BootstrapMethods 类文件 JDK1.7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符

对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。一个符合规则的属性表应该满足如下定义的结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length
u1 info attribute_length
  • Code属性

    Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构如下表所示

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | attribute_name_index | 1 | | u4 | attribute_length | 1 | | u2 | max_stack | 1 | | u2 | max_locals | 1 | | u4 | code_length | 1 | | u1 | code | code_length | | u2 | exception_table_length | 1 | | exception_info | exception_table | exception_table_length | | u2 | attributes_count | 1 | | attribute_info | attributes | attributes_count |

    attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为”Code”,它代表了该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节

    max_stack代表了操作数栈深度的最大值,在方法执行的任意时刻,操作数栈都不会超过这个深度,虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度

    max_locals代表了局部变量表所需的存储空间,在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位,对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数(包括实例方法中的隐藏参数”this”)、显式异常处理器的参数(try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放

    code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。每个指令都是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解

    关于code_length虽然是一个u4类型的长度值,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,即它实际上只使用了u2的长度值

    Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据

  • Exceptions属性

    Exceptions属性的作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常,它的结构如下

    | 类型|名称|数量|类型|名称|数量| | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | | u2 | attribute_name_index | 1 | u2 | number_of_exceptions | 1 | | u4 | attribute_length | 1 | u2 | exception_index_table | number_of_exceptions |

    Exceptions属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型

  • LineNumberTable属性

    LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候也无法按照源码行来设置断点,其属性结构见下表

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | attribute_name_index | 1 | | u4 | attribute_length | 1 | | u2 | line_number_table_length | 1 | | line_number_info | line_number_table | line_number_table_length |

    line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括了start_pc和line_number_info两个u2类型的数据项,前者是字节码行号,后者是Java源码行号

  • LocalVariableTable属性

    LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息,如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下中获取参数值,LocalVariableTable属性的结构如下表

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | attribute_name_index | 1 | | u4 | attribute_length | 1 | | u2 | local_variable_table_length | 1 | | local_variable_info | local_variable_table | local_variable_table_length |

    其中,local_variable_info项目代表了一个栈帧与源码中的局部变量的关联,结构如下表

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | start_pc | 1 | | u2 | length | 1 | | u2 | name_index | 1 | | u2 | descriptor_index | 1 | | u2 | index | 1 |

    start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围

    name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符

    index是这个局部变量在栈帧局部变量表中Slot的位置,当这个变量数据类型是64位类型时(double和long),它占用的Slot为index和index+1两个

  • SourceFile属性

    SourceFile属性用于记录生成这个Class文件的源码文件名称,这个属性也是可选的,可以分别使用Javac的-g:none选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当异常抛出时,堆栈中将不会显示出错代码所属的文件名,这个属性是一个定长的属性,结构如下表

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | attribute_name_index | 1 | | u4 | attribute_length | 1 | | u2 | sourcefile_index | 1 |

    sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名

  • ConstantValue属性

    ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类变量)才可以使用这项属性。虚拟机对于非static类型的变量(也就是实例变量)的赋值是在实例构造器\方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器\方法中或者使用ConstantValue属性。目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个常量,并且这个常量的数据类型是基本数据类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本数据类型及字符串,则将会选择在\方法中进行初始化。ConstantValue属性结构如下

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | attribute_name_index | 1 | | u4 | attribute_length | 1 | | u2 | constantvalue_index | 1 |

    从数据结构中可以看出,ConstantValue属性是一个定长属性,它的attribute_length数据项值必须固定为2,constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种

  • InnerClasses属性

    InnerClasses属性用于记录内部类与宿主类之间的关联,如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性,该属性的结构如下表

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | attribute_name_index | 1 | | u4 | attribute_length | 1 | | u2 | number_of_classes | 1 | | inner_classes_info | inner_classes | number_of_classes |

    数据项number_of_classes代表需要记录多少个内部类信息,每一个内部类的信息都由一个inner_classes_info表进行描述,表结构如下

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | inner_class_info_index | 1 | | u2 | outer_class_info_index | 1 | | u2 | inner_name_index | 1 | | u2 | inner_class_access_flags | 1 |

    inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分别代表了内部类和宿主类的符号引用

    inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为0

    inner_class_access_flags是内部类的访问标志,类似于类的access_flags,它的取值范围如下表

    | 标志名称|标志值|含义| | :-----: | :-----: | :-----: | | ACC_PUBLIC | 0x0001 | 内部类是否为public | | ACC_PRIVATE | 0x0002 | 内部类是否为private | | ACC_PROTECTED | 0x0004 | 内部类是否为protected | | ACC_STATIC | 0x0008 | 内部类是否为static | | ACC_FINAL | 0x0010 | 内部类是否为final | | ACC_INTERFACE | 0x0020 | 内部类是否为synchronized | | ACC_ABSTRACT | 0x0400 | 内部类是否为abstract | | ACC_SYNTHETIC | | 内部类是否并非由用户代码产生的 | | ACC_ANNOTATION | 0x2000 | 内部类是否是一个注解 | | ACC_ENUM | | 内部类是否是一个枚举 |

  • Deprecated及Synthetic属性

    Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念

    Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated注释来进行设置

    Synthetic属性代表此字段或者方法并不是由Java源代码直接产生的,而是由编译器自行添加的,在JDK 1.5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC标志位,其中最典型的例子就是Bridge Method,所有由非用户代码产生的类、方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器”\”和类构造器”\”方法。Deprecated及Synthetic属性的结构如下表

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | attribute_name_index | 1 | | u4 | attribute_length | 1 |

    其中attribute_length数据项的值必须为0x00000000,因为没有任何属性值需要设置

  • StackMapTable属性

    StackMapTable属性在JDK 1.6发布后增加到了Class文件规范中,它是一个复杂的变长属性,位于Code属性的属性表中,这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器

    StackMapTable属性中包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束,StackMapTable属性的结构见表

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | attribute_name_index | 1 | | u4 | attribute_length | 1 | | u2 | number_of_entries | 1 | | stack_map_frame | stack_map_frameentries | number_of_entries |

    在版本号大于或等于50.0的Class文件中,如果方法的Code属性中没有附带StackMapTable属性,那就意味着它带有一个隐式的StackMap属性,这个StackMap属性的作用等同于number_of_entries值为0的StackMapTable属性。一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常

  • Signature属性

    Signature属性在JDK 1.5发布后增加到了Class文件规范之中,它是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中,Signature属性的结构见表

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | attribute_name_index | 1 | | u4 | attribute_length | 1 | | u2 | signature_index | 1 |

    其中signature_index项的值必须是一个对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示类签名、方法类型签名或字段类型签名

  • BootstrapMethods属性

    BootstrapMethods属性在JDK 1.7发布后增加到了Class文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。BootstrapMethods属性的结构如下表

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | attribute_name_index | 1 | | u4 | attribute_length | 1 | | u2 | num_bootstrap_methods | 1 | | bootstrap_method | bootstrap_methods | num_bootstrap_methods |

    其中引用到的bootstrap_method结构如下表

    | 类型|名称|数量| | :-----: | :-----: | :-----: | | u2 | bootstrap_method_ref | 1 | | u2 | num_bootstrap_arguments | 1 | | u2 | bootstrap_arguments | num_bootstrap_arguments |

    BootstrapMethods属性中,num_bootstrap_methods项的值给出了bootstrap_methods[]数组中的引导方法限定符的数量。而bootstrap_methods[]数组的每个成员包含了一个指向常量池CONSTANT_MethodHandle结构的索引值,它代表了一个引导方法,还包含了这个引导方法静态参数的序列(可能为空)。bootstrap_methods[]数组中的每个成员必须包含以下3项内容

    • bootstrap_method_ref:该项的值必须是一个对常量池的有效索引,常量池在该索引处的值必须是一个CONSTANT_MethodHandle_info结构
    • num_bootstrap_arguments:该项的值给出了bootstrap_arguments[]数组成员的数量
    • bootstrap_arguments:数组中的每个成员必须是一个对常量池的有效索引,常量池在该索引处必须是下列结构之一:CONSTANT_String_info、CONSTANT_Class_info、CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_MethodHandle_info或CONSTANT_MethodType_info

3.字节码指令简介

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码

字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构,由于限制了Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过256条,又由于Class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体数据的结构,如果要将一个16位长度的无符号整数使用两个无符号字节存储起来(将它们命名为byte1和byte2),那它们的值应该是这样的:

(byte1 << 8) | byte2

这种操作在某种程度上会导致解释执行字节码时损失一些性能,但这样做的优势也非常明显,放弃了操作数长度对齐,就意味着可以省略很多填充和间隔符号,用一个字节来代表操作码,也是为了尽可能获得短小精干的编译代码

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

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

3.1 字节码与数据类型

在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。

由于Java虚拟机的操作码长度只有一个字节,所以指令集被故意设计成非完全独立的,有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型

大部分的指令都没有支持整型类型byte、char和short,甚至没有任何指令支持boolean类型,编译器会在编译期或运行期将byte和short类型的数据带符号扩展为相应的int类型数据,将boolean和char类型数据零位扩展为相应的int类型数据。Java虚拟机指令集所支持的数据类型如下表

opcode byte short int long float double char reference
Tipush bipush sipush
Tconst iconst lconst fconst dconst aconst
Tload iload lload fload dload aload
Tstore istore lstore fstore dstore astore
Tinc iinc
Taload baload saload iaload laload faload daload caload aaload
Tastore bastore sastore iastore lastore fastore dastore castore aastore
Tadd iadd ladd fadd dadd
Tsub isub lsub fsub dsub
Tmul imul lmul fmul dmul
Tdiv idiv ldiv fdiv ddiv
Trem irem lrem frem drem
Tneg ineg lneg fneg dneg
Tshl ishl lshl
Tshr ishr lshr
Tushr iushr lushr
Tand iand land
Tor ior lor
Txor ixor lxor
i2T i2b i2s i2l i2f i2d
l2T l2i l2f l2d
f2T f2i f2l f2d
d2T d2i d2l d2f
Tcmp lcmp
Tcmpl fcmpl dcmpl
Tcmpg fcmpg dcmpg
if_TcmpOP if_icmpOP if_acmpOP
Treturn ireturn lreturn freturn dreturn areturn

上表列举了Java虚拟机所支持的与数据类型相关的字节码指令,通过使用数据类型列所代表的特殊字符替换opcode列的指令模板中的T,就可以得到一个具体的字节码指令

3.2 加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容:

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

存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作,除此之外,还有少量指令,如访问对象的字段或数组元素的指令也会向操作数栈传输数据

3.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

3.4 类型转换指令

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

Java虚拟机直接支持(即转换成无需显式的转换指令)以下数值类型的宽化类型转换(即小范围类型向大范围类型的安全转换)

  • int类型到long、float或者double类型
  • long类型到float、double类型
  • float类型到double类型

3.5 对象创建与访问指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令,对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素,这些指令如下:

  • 创建类实例的指令: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

3.6 操作数栈管理指令

Java虚拟机提供了一些用于直接操作数栈的指令,包括:

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

3.7 控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改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

3.8 方法调用和返回指令

方法调用的指令:

  • invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式
  • invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用
  • invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
  • invokestatic指令用于调用类方法(static方法)
  • invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法

3.9 异常处理指令

在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用throw语句显式抛出异常情况外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出

在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的

3.10 同步指令

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的

方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置了,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程,如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放

同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持

编译器必须确保无论方法通过何种方式来完成,方法中调用过的每条monitorenter指令都必须执行其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束

3.11 公有设计和私有实现

Java虚拟机规范描绘了Java虚拟机应有的共同程序存储格式:Class文件格式以及字节码指令集。这些内容与硬件、操作系统以及具体的Java虚拟机实现之间是完全独立的,虚拟机实现者可能更愿意把它们看做是程序在各种Java平台实现之间互相安全地交互的手段

虚拟机实现者只要保证优化后的Class文件依然可以被正确读取,并且包含在其中的语义能得到完整的保持,那实现者就可以选择任何方式去实现这些语义

虚拟机实现者可以使用这种伸缩性来让Java虚拟机获得更高的性能、更低的内存消耗或者更好的移植性,选择哪种特性取决于Java虚拟机实现的目标和关注点是什么。虚拟机实现的方式主要有以下两种:

  • 将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集
  • 将输入的Java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(即JIT代码生成技术)

Java虚拟机应被设计成可以允许有众多不同的实现,并且各种实现可以在保持兼容性的同时提供不同的、新的、有趣的解决方案

点赞(1)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 《深入理解Java虚拟机——JVM高级特性与最佳实践》学习笔记——Java类文件结构

相关推荐