《深入理解java虚拟机》读书笔记(四)类文件结构
- 1.无关性
- 2.Class类文件的结构
- 2.1 常量池[^1]
- 2.2访问标志
- 2.3类索引、父类索引和接口索引集合
- 2.4字段表集合
- 2.5 方法表集合
- 2.6 属性表集合
- 2.6.2 Exceptions属性
- 2.6.3 LineNumberTable属性
- 2.6.4 LocalVariableTable及LocalVariableTypeTable属性
- 2.6.5 SourceFile及SourceDebugExtension属性
- 2.6.6 ConstantValue属性
- 2.6.7 InnerClasses属性
- 2.6.8 Deprecated及Synthetic属性
- 2.6.9 StackMapTable属性
- 2.6.10 Signature属性
- 2.6.11 BootstrapMethods属性
- 2.6.12 MethodParameters属性
- 2.6.13 *模块化*[^3]相关属性
- 2.6.14 运行时注解相关属性
1.无关性
Java虚拟机只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。任何语言的实现者都可以将Java虚拟机作为他们语言的运行基础,以Class文件作为它们产品的交付媒介。
2.Class类文件的结构
根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,其中只有两种数据类型:“无符号数”和“表”。
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数值量或者按照UTF-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作一张表,这张表由表2-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 |
无论是无符号数还是表,统一使用一个前置的容量计数器加若干个连续的数据项的形式来描述同一类型但数量不定的多个数据,此时称这一系列连续的某一类型的数据为某一类型的“集合”。
- 1~4字节:魔数,确定这个文件是否为一个能被虚拟机接收的Class文件,值为0xCAFEBABE;
- 5~6字节:Class文件的次版本号;
- 7~8字节:Class文件的主版本号;
- 9~10字节:常量池容量计数值,索引值从1开始计数,标识着常量池中有多少项常量。
2.1 常量池1
常量池容量计数值之后是常量信息,常量池中主要存放两大类常量:字面量和符号引用。字面量接近Java语言层面的常量,如文本字符串,final常量值,符号引用2属于编译原理方面的概念。
常量池中每一项常量都是一个表,截至JDK13,常量表中共有17种结构各不相同的表结构数据,表结构起始的第一位是个u1类型的标志位,代表当前常量属于哪种常量类型。
2.2访问标志
访问标志用于识别一些类或者接口层次的访问信息,如class、interface、public、abstract、final等java关键字。
2.3类索引、父类索引和接口索引集合
类索引和父类索引都是一个u2类型的数据,接口索引集是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。接口索引集合用来描述这个类实现了哪些接口。
2.4字段表集合
字段表用于描述接口或者类中声明的变量。Java语言中的“字段”包括类级变量和实例级变量,但不包括在方法内部声明的局部变量。
- fields_count: 容量计数器,代表这个类中有几个字段表数据
- access_flags:u2数据类型,放置字段修饰符
- name_index: 对常量池项的引用,代表字段的简单名称
- descriptor_index:对常量池项的引用,代表字段和方法的描述符
- attributes:属性表集合,用于附加描述零至多项的额外信息,如变量的初始值
2.5 方法表集合
Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,依次包括访问标志、名称索引、描述符索引和属性表集合几项,仅在访问标志和属性表集合的可选项中有所区别,方法中的代码经过javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为“Code”的属性里面。
在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名。Java代码的特征签名包括方法名称、参数顺序及参数类型(而字节码的特征签名还包括方法返回值及受查异常表)。
2.6 属性表集合
Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。属性表集合中不再要求各个属性表具有严格顺序。
2.6.1Code属性
Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,如接口或者抽象类中的方法就不存在Code属性。
- attribute_name_index:指向CONSTANT_Utf8_info型常量的索引,此常量固定为"Code",它代表了该属性的属性名称;
- attribute_length:指示了属性值的长度,固定为整个属性表长度减去6个字节(属性名称索引与属性长度共6字节);
- max_stack:操作数栈深度的最大值;
- max_locals:局部变量表所需的存储空间,它的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。
- code_length:字节码长度
- code:用于存储字节码指令的一系列字节流。每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,及这条指令后面是否需要跟随参数,如何解析参数。
2.6.2 Exceptions属性
Exception属性是在方法表中与Code属性平级的一项属性,作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。
- number_of_exceptions:表示方法可能抛出number_of_exceptions种受查异常
- exception_index_table:每一种受查异常使用一个exception_index_table项表示,是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。
2.6.3 LineNumberTable属性
用于描述Java源码行号与字节码行号之间的对应关系,非运行时必须的属性,默认生成到Class文件之中,只有生成该属性时,抛出异常时堆栈中会显示出错的行号,才可以设置断点调试。
2.6.4 LocalVariableTable及LocalVariableTypeTable属性
用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,非运行时必须属性。当没有生成时,所有的参数名称将会丢失,使用arg0、arg1之类的占位符代替原有的参数名。
2.6.5 SourceFile及SourceDebugExtension属性
用于记录生成这个Class文件的源码文件名称,非必须属性,指示出错代码所属的文件名。
2.6.6 ConstantValue属性
通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可以使用这项属性。
2.6.7 InnerClasses属性
用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。
- number_of_classes:代表需要记录多少个内部类信息;
- inner_classes_info:每一个内部类的信息都由一个inner_classes_info表进行描述。
2.6.8 Deprecated及Synthetic属性
两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。Deprecated属性用于表示某个类、字段或者方法已经被程序作者定为不再推荐使用;Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。
2.6.9 StackMapTable属性
在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
2.6.10 Signature属性
用于记录泛型签名信息。因为Java语言的泛型采用的是擦除法实现的伪泛型,字节码中所有的泛型信息编译(类型变量、参数化类型)在编译之后都通通被擦除掉。擦除法的好处是实现简单、非常容易实现Backport,运行期也能够节省一些类型所占用的内存空间。
2.6.11 BootstrapMethods属性
用于保存invokedynamic指令引用的引导方法限定符。
2.6.12 MethodParameters属性
记录方法的各个形参名称和信息,使得编译器可以将方法名称也写进Class文件中,且MethodParameters是方法表的属性,与Code属性平级,可以运行时通过反射API获取。
2.6.13 模块化3相关属性
JDK9的一个重量级功能是Java的模块化功能,因为模块描述文件(module-info.java)最终是要编译成一个独立的Class文件来存储的,所以Class文件格式也扩展了Module、ModulePackages和ModuleMainClass三个属性用于支持Java模块化相关功能。
- Module:是一个非常复杂的变长属性,除了表示该模块的名称、版本、标志信息以外,还存储了这个模块requires、exports、opens、uses和provides定义的全部内容;
- ModulePackages:另一个用于支持Java模块化的变长属性,用于描述该模块中所有的包;
- ModuleMainClass:定长属性,用于确定该模块的主类。
2.6.14 运行时注解相关属性
JDK5时期Java语言的语法进行了多项增强,其中之一是提供了对注解的支持,为了存储源码中注解信息,Class文件同步增加了RuntimeVisibleAnnotations等4个属性。JDK8时期进一步加强了Java语言的注解使用范围,又新增类型注解(JSR308),所以Class文件中也同步增加了RuntimeVisibleTypeAnnotations和RuntimeInvisibleTypeAnnotations两个属性。
以RuntimeVisibleAnnotations为例,它是一个变长属性,记录了类、字段或方法的声明上记录运行时可见注解,当我们使用反射API来获取类、字段或方法上的注解时,返回值就是通过这个属性来取到的。