Java虚拟机(8)Class文件结构

无关性

Oracle公司以及其他虚拟机发行商发布过许多可以运行在各种不同硬件平台和操作系统上的Java虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的“一次编写,到处运行”的平台无关性。

实现语言无关性的基础仍然是虚拟机和字节码存储格式,Java虚拟机不与任何程序语言(包括Java)绑定,它只与“Class文件”这种特定的二进制文件格式所关联。Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。基于安全方面的考虑,《Java虚拟机规范》中要求在Class文件必须应用许多强制性的语法和结构化约束,但图灵完备的字节码格式,保证了任意一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。作为一个通用的、与机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为他们语言的运行基础,以Class文件作为他们产品的交付媒介。

例如,使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其他语言的编译器一样可以把它们的源程序代码编译成Class文件。虚拟机不关心Class的来源是什么语言,它与程序语言之间的关系如图所示:

Java虚拟机语言无关性.png

Class类文件结构

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储。

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

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表,这张表由表6-1所示的数据项按严格顺序排列构成。
类型 名称 中文名 数量
u4 magic 文件标志,魔数 1
u2 minor_version 次版本号 1
u2 major_version 主版本号 1
u2 constant_pool_count 常量池容量计数值 1
cp_info constant_pool 常量池 constant_pool_count-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

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,称这一系列连续的某一类型的数据为某一类型的“集合”。

魔数与Class文件版本

每个Class文件的头4个字节称为魔数(magic),它的唯一作用是判断该文件是否为一个能被虚拟机接受的Class文件。它的值固定为0xCAFEBABE。

紧接着magic的4个字节存储的是Class文件的次版本号和主版本号,高版本的JDK能向下兼容低版本的Class文件,但不能运行更高版本的Class文件。

常量池

主、次版本号之后是常量池入口,是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,还是在Class文件中第一个出现的表类型数据项目。

常量池是从1开始索引的,因为第0项是用于某些指向常量池的索引值的数据在特定情况下表示“不引用任何一个常量池项目”的含义。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(SymbolicReferences)。

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

  • 被模块导出或者开放的包(Package)
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符
  • 方法句柄和方法类型(Method Handle、Method Type、InvokeDynamic)
  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。常量表中分别有17种不同类型的常量,这17类表都有一个共同的特点,表结构起始的第一位是个u1类型的标志位(tag),代表着当前常量属于哪种常量类型,17种常量类型所代表的具体含义如表所示:

类型 标志(tag) 描述
CONSTANT_utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 长整型字面量
CONSTANT_Double_info 双精度浮点型字面量
CONSTANT_Class_info 类或接口的符号引用
CONSTANT_String_info 字符串类型字面量
CONSTANT_Fieldref_info 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MothodType_info 16 表示方法类型
CONSTANT_Dynamic_info 17 表示一个动态计算常量
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点
CONSTANT_Module_info 19 表示一个模块
CONSTANT_Package_info 20 表示一个模块中开放或者导出的包

17种常量项的结构定义总结如下:

17项常量项结构.png

访问标志

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

标志名称 标志值 含义
ACC_PUBLIC 0x001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识这是啥一个注解
ACC_ENUM 0x4000 标识这是一个枚举
ACC_MODULE 0x8000 标识这是一个模块

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

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

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

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

接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列在接口索引集合中。

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

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

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

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

字段修饰符放在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

name_index和descriptor_index它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符。

简单名称”,“描述符”以及“全限定名”三种特殊字符串的概念:

  • 类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”号表示全限定名结束。
  • 简单名称则就是指没有类型和参数修饰的方法或者字段名称,假设类中的inc()方法和m字段的简单名称分别就是“inc”和“m”。
  • 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录成“[I”。

方法表集合

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

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

访问标志和属性表集合的可选项中有所区别,如下表:

标志名称 标志值 含义
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 方法是否由编译器自动生成的

属性表集合

属性表(attribute_info),Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 JDK 6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature 类、方法表、字段表 JDK 5中新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要用这个属性记录泛型中的相关信息
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 JDK 6中新增的属性,SourceDebugExtension用于存储额外的调试信息。
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成
LocalVariableTypeTable JDK 5中新增的属性,它使用特征前面代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations 类、方法表、字段表 JDK 5中新增的属性,为动态注解提供支持。用于知名哪些注解是运行时可见的
RuntimeInvisibleAnnotations 类、方法表、字段表 JDK 5中新增的属性,与RuntimeVisibleAnnotations作用正好相反,用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotations 方法表 JDK 5中新增的属性,作用与RuntimeVisibleAnnotations类似,不过作用对象是方法参数
RuntimeInvisibleParameterAnnotations 方法表 JDK 5中新增的属性,作用与RuntimeInvisibleAnnotations类似,不过作用对象是方法参数
AnnotationsDefault 方法表 JDK 5中新增的属性,用于记录注解类元素的默认值
BootstrapMethods 类文件 JDK 7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符
RunntimeVisibleTypeAnnotations 类、方法表、字段表,Code属性 JDK 8中新增的属性,为实现JSR 308中新增的类型注释提供支持,用于用于指明哪些类注释是运行时(实际上运行时就是进行反射调用)可见的
RunntimeInvisibleTypeAnnotations 类、方法表、字段表,Code属性 JDK 8中新增的属性,为实现JSR 308中新增的类型注释提供支持,用于用于指明哪些类注释是运行时不可见的
MethodParameters 方发表 JDK 8新增属性,用于支持(编译时加上-parameters参数)将方法名称编译进Class文件中,并可运行时获取。此前要获取方法名称只能通过JavaDoc中得到
Module JDK 9新增属性,用于记录一个Module的名称以及相关信息(requires、exports、opens、uses、provides)
ModulePackages JDK 9新增属性,用于记录一个模块中所有被exports或者opens的包
ModuleMainClass JDK 9新增属性,用于指定一个模块的主类
NestHost JDK 11新增属性,用于支持嵌套类(Java内部类)的反射和控制访问的API,一个内部类通过该属性的值自己的宿主类
NestMembers JDK 11新增属性,用于支持嵌套类(Java内部类)的反射和访问控制的API,一个宿主类通过该属性得知自己有哪些内部类

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

一个符合规则的属性表应该满足如图结构:

类型 名称 数量
u2 attribute_name_index 1
u4 attributes_length 1
u1 info attributes_length

1)Code属性

Java程序方法体中的代码讲过Javac编译后,生成的字节码指令便会存储在Code属性中,但并非所有的方法表都必须存在这个属性,比如接口或抽象类中的方法就不存在Code属性。如果方法表有Code属性存在,那么它的结构将如下表所示:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_local 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代表了局部变量表所需的存储空间,它的单位是Slot,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用。

  • code_length和code用来存储Java源程序编译后生成的字节码指令。code用于存储字节码指令的一系列字节流,它是u1类型的单字节,因此取值范围为0x00到0xFF,那么一共可以表达256条指令,目前,Java虚拟机规范已经定义了其中200条编码值对应的指令含义。code_length虽然是一个u4类型的长度值,理论上可以达到2^32-1,但是虚拟机规范中限制了一个方法不允许超过65535条字节码指令,如果超过了这个限制,Javac编译器将会拒绝编译。

  • 字节码指令之后是这个方法的显式异常处理表集合(exception_table),它对于Code属性来说并不是必须存在的。它包含四个字段:

类型 名称 数量
u2 start_pc 1
u2 end_pc 1
u2 handler_pc 1
u2 catch_type 1

这些字段的含义为:如果字节码从第start_pc行到第end_pc行之间(不含end_pc行)出现了类型为catch_type或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理,当catch_pc的值为0时,代表人和的异常情况都要转到handler_pc处进行处理。异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常即finally处理机制,也因此,finally中的内容会在try或catch中的return语句之前执行,并且在try或catch跳转到finally之前,会将其内部需要返回的变量的值复制一份副本到最后一个本地表量表的Slot中。

Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码和元数据两部分,那么在整个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

2)Exception属性

列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。

3)LineNumberTable属性

用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系

4)LocalVariableTable及LocalVariableTypeTable属性

LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,

在JDK 5引入泛型之后,增加了LocalVariableTypeTable属性,使用字段的特征签名来完成泛型的描述。

5)SourceFile及SourceDebugExtension属性

SourceFile属性用于记录生成这个Class文件的源码文件名称。在JDK 5时,新增了SourceDebugExtension属性用于存储额外的代码调试信息。

6)ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。

对非static类型的变量(也就是实例变量)的赋值是在实例构造器()方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器()方法中或者使用ConstantValue属性。

Oracle公司实现的Javac编译器的选择是,如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就将会生成ConstantValue属性来进行初始化;如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在()方法中进行初始化。

7)InnerClasses属性

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

8)Deprecated属性和Synthetic属性

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

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

Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。

9)StackMapTable属性

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

10)Signature属性

Signature属性在JDK 5增加到Class文件规范之中,它是一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中。

11)BootstrapMethods属性BootstrapMethods属性

在JDK 7时增加到Class文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。

12)MethodParameters属性

MethodParameters是在JDK 8时新加入到Class文件格式中的,它是一个用在方法表中的变长属性,作用是记录方法的各个形参名称和信息。

13)模块化相关属性

JDK 9的一个重量级功能是Java的模块化功能,因为模块描述文件(module-info.java)最终是要编译成一个独立的Class文件来存储的,所以,Class文件格式也扩展了Module、ModulePackages和ModuleMainClass三个属性用于支持Java模块化相关功能。

Module属性是一个非常复杂的变长属性,除了表示该模块的名称、版本、标志信息以外,还存储了这个模块requires、exports、opens、uses和provides定义的全部内容

ModulePackages是另一个用于支持Java模块化的变长属性,它用于描述该模块中所有的包,不论是不是被export或者open的。

ModuleMainClass属性是一个定长属性,用于确定该模块的主类(Main Class)。

14)运行时注解相关属性

JDK 5时期,Java语言的语法进行了多项增强,其中之一是提供了对注解(Annotation)的支持。为了存储源码中注解信息,Class文件同步增加了RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations和RuntimeInvisibleParameter-Annotations四个属性。JDK 8,进一步加强了Java语言的注解使用范围,又新增类型注解(JSR 308),所以Class文件中也同步增加了RuntimeVisibleTypeAnnotations和RuntimeInvisibleTypeAnnotations两个属性。这六个属性不论结构还是功能都比较雷同,以RuntimeVisibleAnnotations为例:

RuntimeVisibleAnnotations是一个变长属性,它记录了类、字段或方法的声明上记录运行时可见注解,当我们使用反射API来获取类、字段或方法上的注解时,返回值就是通过这个属性来取到的。

以上就是Java虚拟机关于类文件结构的内容总结,想更详细地了解的话,可以进一步阅读《深入理解Java虚拟机》书籍,获取更多知识。

欢迎点赞/评论,你们的赞同和鼓励是我写作的最大动力!

你可能感兴趣的:(Java虚拟机(8)Class文件结构)