1. 概述
2. 无关性的基石
针对各种不同平台的 Java 虚拟机,以及所有平台都支持的程序存储格式——字节码是构成平台无关性的基石。
Java 在发布之初,把 Java 的规范拆分成了 《Java 语言规范》 和 《Java 虚拟机规范》。
语言无关性也正在被重视。目前跑在 Java 虚拟机上的语言有 Kotlin、Clojure、Groovy、JRuby、 JPython、Scala 等。实现语言无关性的基础仍然是虚拟机和字节码存储格式。
3. 类文件结构
任何一个 Class 文件都对应着唯一一个类或者接口的定义信息。但是类或者接口不一定都得定义到文件中,因为类或者接口也可用动态生成,直接送进类加载器。
Class 文件是一组以 8 个字节为基础单位的二进制流,各个数据项按照顺序紧凑的排列在文件之中,中间没有任何分隔符。这使得整个 Class 文件几乎全部都是程序运行的必要数据,没有空隙。当遇到需要占用 8 个字节以上的空间的数据项时,则会按照高位在前的方式分割成若干个 8 个字节存储。
Class 文件采用一种伪结构来存储数据,这种结构只有无符号数和表两种类型。
无符号数属于基本的数据类型,以 u1、u2、u4、u8 来代表 1、2、4、8 个字节的无符号数,无符号数可用用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串。
下面的表描述的是每种数据项的长度、顺序及数量。
| 类型 | 名称 | 数量 |
|---|---|---|
| u4 | magic | 1 |
| u2 | minor_version | 1 |
| u2 | major_version | 1 |
| u2 | constant_pool_count | 1 |
| cp_info | constant_pool | constant_pool_count |
| 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 | attribute_count | 1 |
| attribute_info | attributes | attribute_count |
3.1 魔数与 Class 文件的版本
每个 Class 文件的开头 4 个字节被称作魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。类似文件名后缀 gif、jpeg 等,后缀是方便人识别,魔是方便虚拟机识别,且不容易被随意改动。
魔数后面紧接着第 5、6 个字节是次版本号,第 7、8 个字节是主版本号。次版本号基本没有用,主要使用的是主版本号。
下表是 JDK 版本与主版本号的对应表,其中 -target 标识编译输出的版本号, -source 标识编译的原 .Java 文件的版本号,从 JDK 9 开始,编译器不再支持编译版本号小于 1.5 的版本。
| JDK 版本 | -target 参数 | -source 参数 | 版本号 |
|---|---|---|---|
| JDK 1.1.8 | 不支持target参数 | 不支持 source 参数 | 45.3 |
| JDK 1.2.2 | 不带(默认为-target 1.1) | 1.1~1.2 | 45.3 |
| JDK 1.2.2 | -target 1.2 | 1.1~1.2 | 46.0 |
| JDK 1.3.1_19 | 不带(默认为-target 1.1) | 1.1~1.3 | 45.3 |
| JDK 1.3.1_19 | -target 1.3 | 1.1~1.3 | 47.0 |
| JDK 1.4.2_10 | 不带(默认为-target 1.2) | 1.1~1.4 | 46.0 |
| JDK 1.4.2_10 | -target 1.4 | 1.1~1.4 | 48.0 |
| JDK 5.0_11 | 不带(默认为—target 1.5),后续版本不带target参数,默认编译的Class文件均与其JDK版本相同 | 1.1~1.5 | 49.0 |
| JDK 5.0_11 | -target 1.4-source 1.4 | 1.1~1.5 | 48.0 |
| JDK 6 | 不带(默认为-target 6) | 1.1~6 | 50.0 |
| JDK 7 | 不带(默认为-target 7) | 1.1~7 | 51.0 |
| JDK 8 | 不带(默认为-target 8) | 1.1~8 | 52.0 |
| JDK 9 | 不带(默认为-target9) | 6~90 | 53.0 |
| JDK 10 | 不带(默认为-target 10) | 6~10 | 54.0 |
| JDK 11 | 不带(默认为-target 11) | 6~11 | 55.0 |
| JDK 12 | 不带(默认为-target 12) | 6~12 | 56.0 |
| JDK 13 | 不带(默认为-target 13) | 6~13 | 57.0 |
3.2 常量池
根据前面的数据项表,在主版本后,就是 u2 类型 2 个字节的常量池计数。这个计数是从 1 开始的,0 项被设计来表达:不引用任何一个常量池项目。Class 文件只有常量池计数是从 1 开始,其他的集合类型都是从 0 开始计数。
上图中,常量池计数是 22 但是从 1 开始,所以一共有 21 个常量。
常量池主要存放两大类常量:
- 字面量:接近 Java 语言层面的常量概念。
- 字符串。
- final 修饰的常量值。
- 符号引用:属于编译原理方面的概念。
- 被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点额动态常量
常量池中的每一项常量都是一共表,最初一共有 11 种结构,为了支持动态语言额外增加了 4 种动态语言相关的常量。为了支持 Java 模块化,增加了 2 种常量,所以目前一共有 17 种常量。
下图是每一种常量结构及描述。
下面,我们基于上面的常量池结构表来读前面的常量: 首先,由于 17 项常量的都有一个 tag ,且 tag 是一个 u1 的一个字节的数字。所以在常量池数量 0016 后面的 07 就表示对应十进制的 7 的常量项。
查上面的表可以发现对应的常量是 CONSTANT_Class_info ,根据 CONSTANT_Class_info 的结构,tag 后面紧接着是 u2 两个字节的的 name_index 值为 0002。它表示指向常量池中的第 2 个常量(从前面可知,当前例子一共有 21 个常量)。
接着看第二个常量,由于 tag 是 u1 一个字节长度。所以紧接着 0002,我们可以读到 01 。用 01 去上面的常量池表查询可以发现对应的常量是 CONSTANT_Utf8_info。紧接着是 u2 个长度 001D 转换成十进制是 29,也就是接下来 29 个字节是 utf8 字符常量。读出来是 "org/fenixsoft/clazz/TestClass"。
按照上面的步骤,可以继续读 29 位之后的 07 进行查表得到 CONSTANT_Class_info。
上面有一个巧合:第一个常量的第二个位置(0002)刚好指向第二个常量,在实际情况中可以是指向常量池中的任意个常量。
我们可以用 javap 输出 Class 文件的字节码内容。命令:javap -verbose TestClass
3.3 访问标志
在常量池结束后,紧接着是 2 个字节的访问标志。这些信息包括:这个 Class 是类还是接口,是否是 public 类型,是否是 abstract 类型,是否被声明为 final 等等。
下面是访问标志位的具体含义:
访问标志一共有 16 个标志位可以使用,当前只定义了 9 个,没有使用的标志位为零。
对于一个普通类,如果它是 public 的,但是没有被 final 和 abstract 修饰,那么它的 ACC_PUBLIC、ACC_SUPER 标志位为真,而 ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、 ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、 ACC_MODULE 这七个标志为假。此时它的访问标志值为:0x0001|0x0020=0x0021
3.4 类索引、父类索引与接口索引集合
类索引(this_class)、父类索引(super_class)是 u2 两个字节的数据。接口索引集合(interfaces)是一组 u2 两个字节数据的集合。类索引确定了当前类的全限定名,父类索引确定了父类的全限定名。Java 是单继承,除了 Object 类外,所有的类的父类索引都不为零。接口索引集合中按照 implement (Class 是接口时是 extends) 关键字后的顺序从左到右排列在接口索引集合中。
类索引、父类索引、接口索引集合都是按照顺序排列在访问标志之后,类索引和父类索引都是 u2 类型索引值,这个索引值指向常量池对应的第索引值个常量,例如类索引值为:0x0001,父类索引值为:0x0003,分别表示,类索引值对应常量池中第一个常量,父类索引值对应常量池中第三个常量。接口索引集合也是类似的查找方式。
3.5 字段表集合
字段表(field_info)用于描述接口或者类中申明的变量。字段表中的变量包括类级变量和实例级变量。但是不包括方法内的局部变量。
变量、常量、字面量解释:
int a = 1;
final int b = 2;
String c = "abc";
final String d = "bcd";
对于上面这几行代码:其中 a、c 是变量,b、d 是常量。其中 1、2、"abc"、"bcd" 是字面量。
字段包括:
- 作用域:public、private、 protected
- 实例变量还是类变量:static
- 可变性:final
- 是否可被序列化:transient
- 并发可见性:volatile
- 字段数据类型:基本类型、对象、数组
- 字段名称
下面是字段表的结构:
下面是字段访问标志表:
由于 Java 语法约束:ACC_PUBLIC、ACC_PRIVATE、 ACC_PROTECTED 三个标志只能选一个,ACC_FINAL、 ACC_VOLATILE 不能同时选。接口中的字段必须有:ACC_PUBLIC、 ACC_STATIC、ACC_FINAL 标志。
访问标志后面是两项索引值:name_index 和 descriptor_index 两项。它们都指向常量池中对应的第索引值项常量。
- 全限定名:例子:
org/fenixsoft/clazz/TestClass,将类全名中的"."替换成"/"就是全限定名。 - 简单名:对于方法
test()的简单名称是:test, 对于字段m的简单名称就是m。 - 描述符:描述符是描述字段的数据类型、方法的参数列表和返回值。
下面是描述符标识含义表:
下面给出描述符的具体例子:
对于数组类型,使用的是前置的 "[" 字符描述,比如:"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。
下面给出一个查字段表的示例:
由第一个表可知,在 interfaces 之后就是 u2 类型的 fields_count,接着是 field_info ,继续查字段表结构得知,field_info 首先是 u2 的 access_flags,接着是 u2 的name_index, 再接着是 u2 的 descriptor_index。其中 name_index 和 descriptor_index 的值是指向常量池对应的第几个常量池对象。比如:比如 name_index 的值是 0x0005 表示 name_index 指向常量池第五个对象。字段 descriptor_index 之后,可以有 attributes。 attributes 在后面的章节详细介绍。
字段表集合中不会列出父类或者父接口中继承来的字段,但是可能出现原本 Java 代码中不存在的字段,比如在内部类中为了保持对外部类的访问性,编译器会自动添加指向外部类实例的字段。在 Java 中字段是无法重载的,两个字段不管数据类型、修饰符是否相同,都必须使用不一样的名称。但是对 Class 文件来说,只要两个字段的描述符不完全相同,那字段重名就是合法的。
3.6 方法表集合
Class 文件中,对方法的描述与对字段的描述几乎完全一致。方法表的结构与字段表的结构一样,下图是方法表结构:
由于 volatile 和 transient 关键字不能修饰方法,所以方法表的访问标识没有这两个标志。但是方法可以被 synchronized、native、strictfp、abstract 修饰,也添加了对应的标志。下图是访问标志表:
其中方法中的代码被编译后,放在了方法的属性集合中一个名字叫 "Code" 的属性里面了。
下面是查方法表的示例:
首先是 u2 的 methods_count 值为 0x0002 表示有两个方法。接着查方法结构表,按照方法结构表,首先是 u2 的 access_flags 值是0x0001 也就是 ACC_PUBLIC 为真。接着是 name_index 值为 0x0007 指向常量池第 7 个常量。 descriptor_index 值为 0x0008 指向常量池第八个常量。接着是 attribute_count 值为 0x0001 表示有一个属性,attribute_name_index 值为 0x0009 表示指向常量池第九个常量。
如果父类方法在子类中没有被重写,方法表就不会出现来自父类方法的信息。但是可能出现编译器自动添加的方法,比如类构造器<clinit()>和实例构造器<init>()。
在 Java 中,重载一个方法要求方法名称相同,同时还要有不同的方法签名。方法的签名包括方法参数顺序和参数类型。但是不包含返回值,所以 Java 无法通过不同的返回值来重载方法。 但是在 Class 文件中,只要方法的描述符不完全一样,两个方法就可以共存,也就说返回值不一样的两个相同方法是合法的。
3.7 属性表集合
Class 文件、字段表、方法表都可以携带自己的属性表集合。
JVM 虚拟机规范不要求各个属性表具有严格的顺序,并且要求只要不与已有属性名重复,任何人实现编译器都可以向属性表中写入自定义的属性信息。JVM 虚拟机规范最初只定义了 9 项虚拟机应该识别的属性,在 Java SE 12 中新增加到 29 项。
下面是虚拟机预定义的属性:
一个符合规则的属性表:
一个符合规则的属性表,它的 attribute_name_index 是指向常量池中一个 CONSTANT_Utf8_info 类型的常量。attribute_length 描述了整个属性的长度。那么属性值的长度就为 attribute_length 的值减去 6 个字节(attribute_name_index 和 attribute_length 的长度)。
1. Code 属性
Java 方法体里面的代码经过编译后的字节码存储在 Code 属性里。但是并不是所有的方法表都需要 Code 属性,比如接口或者抽象类中的方法就可以不存在 Code 属性。
下面是 Code 属性的表结构:
它的 attribute_name_index 指向 CONSTANT_Utf8_info 类型的常量的索引,这个常量的值固定为 "Code", attribute_length 表示了次属性的长度需要减去 6 个字节。
max_stack 表示最大的操作数栈值。虚拟机会根据这个值给栈帧分配操作数栈的深度。
max_locals 表示局部变量表所需的最大存储空间,max_locals 的单位是变量槽(Slot),对于长度不超过 32 位的数据类型,比如 byte、char、int等每个局部变量占用一个槽,double 和 long 这两个 64 位数据类型占用两个槽。方法参数(包括实例方法中隐藏的 this 参数)、显示的异常处理参数(catch 块中的异常)、方法体中的局部变量都需要依赖局部变量表来存放。 JVM 会根据局部变量的作用域来实现复用变量槽,当代码执行超出作用域后,变量槽可以被复用。因此 max_locals 的值是根据同时存在的最大局部变量数量和类型计算出来的。
code_length 和 code 用来存储字节码指令。code_length 表示字节码长度,code 用于存储字节码指令的字节流。其中每个指令的长度是 u1 类型的单字节。当虚拟机读取到指令后,就可以知道这条指令后面是否需要参数以及如何解析参数。u1 对于的取值范围是 0~255,也就是可用表达 256 条指令,但是 JVM 规范现在定义了约 200 条指令。code_length 的最大取值可以达到 2 的 32 次幂,但是 JVM 规范限制方法不能超过 65535 条字节码指令,即只使用了 u2 的长度,如果超过这个长度,就会编译报错,一般不刻意写超长的方法,就不会报错。
Code 属性是 Class 文件中最重要的一个属性,如果把 Java 程序中的信息分为代码(Code,方法体里的 Java 代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个 Class 文件里,Code 属性用于描述代码,其他所有数据项都用于描述元数据。
在 Java 中,在实例方法里可以通过 this 关键字访问到此方法所属的对象。这个机制是通过 Javac 编译器编译的时候把 this 关键字转换成一个普通方法参数在虚拟机调用实例方法时自动传入而实现的。
在字节码指令之后是这个方法的显示的异常处理表,异常表对于 Code 属性不是必须存在的。
下面是异常表结构:
字段含义:如果从 start_p 行开始,到第 end_pc(不包含)行之间出现了类型为 catch_type 或者其子类的异常,则转到第 handler_pc 行继续处理。当 catch_type 的值为 0 表示任何异常情况都要转到 handler_pc 进行处理。
public int inc() {
int x;
try {
x = 1;
// more logic ...
return x;
} catch (Exception e) {
x = 2;
// more logic ...
return x;
} finally {
x = 3;
}
}
上面代码的三条执行路径:
- try 块中出现 Exception 或者它的子类型异常,转到 catch 块处理。
- try 块中出现不属于 Exception 或它的者子类型异常,转到 finally 块处理。
- catch 块出现任何异常,转到 finally 块处理。
上面的代码返回值:如果没有异常,返回 1,如果有 Exception 或者它的子类型异常返回 2,如果有不属于 Exception 或者它的子类型异常,或者 catch 快出现异常,方法非正常退出,没有返回值。finally 块可用有返回值,但是不建议使用,会掩盖上面代码的错误。
2. Exceptions 属性
Exceptions 属性是在方法表中,与 Code 属性平级的一项属性,与前面的 异常表不一样。 Exceptions 的作用是列举出方法中可能抛出的受检异常,也就是在 throws 关键字后面列举的异常。
number_of_exceptions 表示可能抛出的异常种类数,exception_index_table 表项执行常量池中的 CONSTANT_Class_info 类型常量的索引。
3. LineNumberTable 属性
LineNumberTable 属性用来描述 Java 源码行号与字节码行号之间的关系,它不是运行时必须的属性,但是会默认生成到 Class 文件中,可以vac 中使用-g:none 或-g:lines 选项来取消或要求生成这项信息。如果选择不生成 LineNumberTable 属性,对程序运行产生的最主要影响就是当抛出异常时,堆栈中将不 会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
line_number_info 表包含 start_pc 和 line_number 两个 u2 类型的数据项,前者是 字节码行号,后者是 Java 源码行号。
4.LocalVariableTable 及 LocalVariableTypeTable 属性
LocalVariableTable 属性用于描述栈帧中局部变量表的变量与 Java 源码中定义的变量 之间的关系,它也不是运行时必需的属性,但默认会生成到 Class 文件之中,可以在 Javac 中使用-g:none 或-g:vars 选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,譬如 IDE 将会使用诸如 arg0、arg1 之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。LocalVariableTable 属性的结构如表 6-19 所示。
其中 local_variable_info 项目代表了一个栈帧与源码中的局部变量的关联,结构如表 6-20 所示。
start_pc 和 length 属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其 作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。
name_index 和 descriptor_index 都是指向常量池中 CONSTANT_Utf8_info 型常量的 索引,分别代表了局部变量的名称以及这个局部变量的描述符
index 是这个局部变量在栈帧的局部变量表中变量槽的位置。当这个变量数据类型 是 64 位类型时(double 和 long),它占用的变量槽为 index 和 index+1 两个。
在 JDK 5 引入泛型之后,LocalVariableTable 属性增加了一个“姐妹属 性”—— LocalVariableTypeTable。这个新增的属性结构与 LocalVariableTable 非常相似, 仅仅是把记录的字段描述符的 descriptor_index 替换成了字段的特征签名(Signature)。 对于非泛型类型来说,描述符和特征签名能描述的信息是能吻合一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确描述泛型类型了。因此出现了 LocalVariableTypeTable 属性,使用字段的特征签名来完成泛型的描述。
5.SourceFile 及 SourceDebugExtension 属性
SourceFile 属性是用于记录生成这个 Class 文件的源码文件名称。这个属性也是可选的,可以使用 javac 的 -g:none 或者 -g:source 选项来关闭或者生成这项信息。对大多数类来说,类名和文件名称是一致的,但是一些特殊情况(如:内部类)例外。如果不生成这项属性,在抛出异常时,堆栈中将不会显示出错误代码所属的文件名称。
下面是 SourceFile 属性表结构:
其中 sourcefile_index 指向常量池中 CONSTANT_Utf8_info 型常量的索引,常量值是源文件的文件名。
为了方便在编译器和动态生成的 Class 中加入供程序员使用的自定义内容,新增了 SourceDebugExtension 属性,用于存储额外的代码调试信息,典型的场景是进行 JSP 文件调试时,无法通过 Java 堆栈来定位到 JSP 文件的行号。
6.ConstantValue 属性
ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值。只有被 static 关键之修饰的变量才可以使用这项属性。JVM 虚拟机给实例变量赋值是在实例构造器 <init>() 方法中进行的。对类变量有两种选择:在类构造器 <clinit>() 方法中或者使用 ConstantValenzuela 属性。目前 Oracle 实现的 javac 编译器的选择是,如果是常量( 用final 和 static 修饰),并且数据类型是基本类型或者 String 将会使用 ConstantValue 来进行初始化。如果没有被 final 修饰,或者并非基本类型及字符串,则会在 <clinit>() 方法中进行。
7.InnerClasses 属性
InnerClasses 属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器会为它及它所包含的内部类生成 InnerClasses 属性。
number_of_classes 代表需要记录多少个内部类信息,每一个内部类信息都由一个 inner_classes_info 表进行描述。
inner_class_info_index 和 outer_class_info_index 都是指向常量池中 CONSTANT_Class_info 型常量的索引,分别代表了内部类和宿主类的符号引用。inner_name_index 是指向常量池中 CONSTANT_Utf8_info 型常量的索引,代表内部类的名称。如果是匿名内部类,这项值为 0。inner_class_access_flags 是内部类的访问标志,它的取值范围如下图:
8.Deprecated 及 Synthetic 属性
Deprecated 和 Synthetic 两个都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。
Deprecated 属性用于表示某个类、方法、字段已经被程序作者定为不再推荐使用,在代码中用 @deprecated 注解进行设置。
Synthetic 属性表示此字段、方法或者类不是由 Java 源码产生的,而是编译器自行添加的。也可以设置它们的访问标志中的 ACC_SYNTHETIC 标志位。编译器通过生成一些在源码中不存在的 Synthetic 方法、字段、类,来实现越权访问或者绕开语言限制的功能,其中典型的例子是:枚举类中自动生成的枚举元素数组和嵌套类的桥接方法。所有不属于用户代码产生的字段、方法、类都至少应该设置 Synthetic 属性或者 ACC_SYNTHETIC 标志位中的一项。唯一的例外是 实例构造器 <init>() 方法和类构造器<clinit>()方法。
Deprecated 及 Synthetic 属性的结构,其中 attribute_length 值必须为 0,因为没有任何需要设置的属性值。
9.StackMapTable 属性
StackMapTable 属性在 JDK 6 增加到 Class 文件规范之中,它位于 Code 属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(TypeChecker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
10.Signature 属性
Signature 属性是一个可选的定长属性,可以出现在类、字段表、方法表结构的属性表中。在 JDK 5 中增加了泛型支持,所以类、接口、初始化方法或者成员的泛型签名如果包含了类型变量或者参数化类型,则 Signature 属性会记录泛型签名信息。之所以要用一个属性去记录泛型,是因为 Java 的泛型采用的是擦除法实现的伪泛型,字节码(Code属性)中所有的泛型信息在编译之后都会被擦除掉。使用擦除法的好处是实现简单,容易实现移植,运行期也能节省一些类型所占用的存储空间。坏处是无法将泛型类型与用户定义的普通类型同等对待,例如运行期做反射无法获得泛型信息,Signature 属性就是为了弥补这个缺陷而增设的,Java 的反射 API 获取到的泛型信息的最终数据来源就是这个属性。
Signature 属性的结构表:
11.BootstrapMethods 属性
BootstrapMethods 属性是在 JDK 7 中增加到 Class 文件规范中的,它是一个复杂的变成属性,位于类文件的属性表中,这个属性用于保存 invokedynamic 指令引用的引导方法限定符。JVM 规范规定,如果某个类文件结构的常量池中出现过 CONSTANT_InvokeDynamic_info 类型常量,那么这个类文件的属性表中必须存在一个明确的 BootstrapMethods 属性,类文件中最多有一个 BootstrapMethods 属性。BootstrapMethods 在 JDK 8 中 Lambda 表达式和接口默认方法出现后才算有了用武之地。
12.MethodParameters 属性
MethodParameters 属性是在 JDK 8 中加入到 Class 文件格式中的,它的作用是记录方法的各个形参名称和信息。最开始方法的名称是放在 LocalVariableTable 属性中的,但是对于抽象方法和接口没有 Code 属性就没有办法保存参数名称。所以增设了 MethodParameters 属性。
13.模块化相关属性
JDK 9 的一个重量级功能就是模块化功能,因为模块描述文件(module-info.java)最终要编译成一个独立的 Class 文件来存储,所以 Class 文件格式扩展了 Module、ModulePackages 和 ModuleMainClass 三个属性用于支持 Java 模块化相关功能。
14.运行时注解相关属性
JDK 5 提供了对注解的支持,为了存储源码中注解信息,Class 文件同步增加了 RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、 RuntimeVisibleParameterAnnotations 和 RuntimeInvisibleParameter-Annotations 四个属性。JDK 8 时期 Class 文件中增加了 RuntimeVisibleTypeAnnotations 和 RuntimeInvisibleTypeAnnotations 两个属性。
RuntimeVisibleAnnotations 是一个变长属性,它记录了类、字段或方法的声明上记 录运行时可见注解,当我们使用反射 API 来获取类、字段或方法上的注解时,返回值就 是通过这个属性来取到的。