JVM Class 文件结构

引言

本文着重介绍 JVM 中 Class 文件相关的内容,更多关于 JVM 的文章均收录于<JVM系列文章>

Class 文件结构

众所周知,Java 虚拟机提供了 Java 语言的跨平台能力,使同一份 Java 代码可以运行在不同的平台上。除此之外,JVM 作为一个平台,还提供了跨语言特性,从理论上说,无论是哪种语言编写的程序,只要能够编译成 Class 文件,就能通过 JVM 运行在各种平台之上。而实现这一特性的关键,就是统一而强大的 Class 文件结构,它是异构语言与 JVM 之间的重要桥梁。当前,诸如 Groovy、Scala、Jython(Python)等语言都能编译成 Class 文件并运行在 JVM 之上。
multiple-language-run-in-jvm
Class 文件的结构并不是一成不变的,随着 JVM 的版本更替,总是不可避免的要对 Class 文件做出一些调整,但是调整归调整,整个 Class 文件的内容组织形式是非常稳定的。接下来,将介绍一下 Class 文件中最常见的内容,Class 文件的整体结构如下图所示。
class-file-content
在 JVM 规范中,Class 文件使用类似于 C 语言结构体的方式进行描述,并且统一使用无符号整数作为基本数据类型,由 u1、u2、u4、u8 分别表示无符号单字节、2 字节、4 字节、8 字节,对于字符串使用 u1 数组进行表示。一个 Class 文件可以非常严谨的被描述为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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];
}

Class 文件的结构严格按照上述结构体定义:

  1. 文件以一个 4 字节的魔数开头,紧跟着两个 2 字节的大小版本号
  2. 版本号之后是常量池,常量池的个数为 constant_pool_count,常量池的表项有 constant_pool_count - 1 个
  3. 常量池之后是类的访问修饰符、代表自身类引用、父类引用以及接口数量和实现的接口引用
  4. 接口之后有字段数量和描述,方法数量以及方法描述
  5. 最后存放着类文件的属性信息

魔数

魔数(Magic Number)是 Class 文件的标志,用来告诉 JVM 这是一个 Class 文件。魔数是一个 4 字节的无符号整数,它固定为 0xCAFEBABE。接下来我们编译如下类 TestClass,来确认编译出来的 Class 文件中魔数的内容。

1
2
3
4
package bbm.jvm;

public class TestClass {
}

编译出来的 Class 文件如下所示,可以看到文件的头 4 个字节就是 0xCAFEBABE。
test-class-1

文件版本

在魔数之后,紧跟着 Class 的小版本号和大版本号,它们都是 2 字节的无符号整数。
test-class-2
因为之前编译 TestClass 时使用的是 JDK 1.8 ,所以编译出来的版本号是 0x34(十进制数为 52),正好符合下面的 Class 文件版本对应表。
class-file-version-map
目前,高版本的 JVM 可以执行由低版本编译器生成的 Class 文件,但是低版本的 JVM 不能执行由高版本编译器生成的 Class 文件,所以在实际的开发中,我们需要特别注意编译时使用的 JDK 版本与生产环境的 JDK 版本是否一致,如果不一致的话,会报出 UnsupportedClassVersionError

常量池

常量池是 Class 文件中内容最为丰富的区域之一。随着 Java 虚拟机的不断发展, 常量池的内容也日渐丰富。同时, 常量池对于 Class 文件中的字段和方法解析也有着至关重要的作用, 可以说, 常量池是整个 Class 文件的基石。在版本号之后, 紧跟着的是常量池的数量, 以及若干个常量池表项。

再回到我们的例子中,TestClass 编译出来的 Class 文件中,constant_pool_count 为 0x10,也就是说这个 Class 文件中包含了 16 - 1 = 15 个常量项(常量池 0 为保留项,不存放实际内容)。
test-class-3
在数量之后,就是常量池中各项的实际内容,不同类型的常量项的内容内容结构各不相同,但是一般都是以 “类型-长度-内容” 或者 “类型-内容” 的格式依次排列。在介绍我们的 TestClass 之前,我们先了解一下常量池项的类型都有哪些。
constant-types
有了从 TAG 到常量池项类型的映射关系之后,我们就能够分析 TestClass 中的那 15 个常量池项都是什么了。回到我们的 TestClass 中,我们可以看到第一个常量池项的类型是 0x0A(10),也就是说该项的类型是 Methodref。而 Methodref 项的结构如下:

1
2
3
4
5
CONSTANT_Methodref_info {
u1 tag; // 类型编号 10
u2 class_index; // 函数所属类对应的 class 常量池项编号
u2 name_and_type_index; // 描述函数的对应的 name_and_type 常量池项编号
}

理解了 CONSTANT_Methodref_info 的数据结构之后,我们就能分析出第一个常量池项的类型是 Methodref,是一个方法的引用,该方法所属的类名存在第 3(0x03)个常量池项中,方法名存储在第 13(0x0D)个常量池项中。
test-class-4
而第二个常量池项的类型编号是 7(0x07),对应的常量池项结构如下:

1
2
3
4
CONSTANT_Class_info {
u1 tag; // 类型编号 7
u2 name_index; // 类名对应的 utf8 常量池项编号
}

理解了 CONSTANT_Class_info 的结构之后,我们就能知道第二个常量池项的的类型是 Class,类名存储在第 14(0x0E)个常量池项中。
test-class-5
第三个常量池项的类型编号也是 7(0x07),说明第三个常量池项也是一个 Class 项,类名存储在第 15(0x0F)个常量池项中。
test-class-6
第四个常量池项的类型编号是 1(0x01),对应的类型是 CONSTANT_Utf8_info,它的结构如下:

1
2
3
4
5
CONSTANT_Utf8_info {
u1 tag; // 类型编号 1
u2 length; // utf8 字符串长度
u1 bytes[length]; // utf8 字符串的内容
}

在 CONSTANT_Utf8_info 中保存的主要内容是字符串的长度,以及字符串的内容。回到我们的例子中,我们可以得出第四个常量池项保存的字符串的长度为 6(0x06),内容为 <init>
test-class-7
从第五个常量池项开始,到第 12 个常量池项,保存的的类型都是 Utf8,它们存储的内容分别为 ()VCodeLineNumberTableLocalVariableTablethisLbbm/jvm/TestClass;SourceFileTestClass.java
test-class-8
第 13 个常量池项的类型是 12(0x0C),对应的数据结构如下:

1
2
3
4
5
CONSTANT_NameAndType_info {
u1 tag; // 类型编号 12
u2 name_index; // 名字对应的 utf8 常量池项编号
u2 descriptor_index; // 描述符对应的 utf8 常量池项编号
}

descriptor 使用了一组特定的字符串来表示类型,如下表所示:
descriptor-table
从上表中我们可以看出,每个基础类型都有一个对应的大写字母,例如 B 对应 byte,D 对应 double。而对象类型总是以 L 开头,紧跟类的全限定名,并用分号(;)结尾,比如本例中的 Lbbm/jvm/TestClass; 表示类 bbm.jvm/TestClass。数组则以左中括号 ‘[‘ 作为标记,比如 String 二维数组则使用 [[Ljava/lang/String; 表示。

也就是说第 13 个常量池项是 NameAndType,其中名字对应的是第 4(0x04) 个常量池项,描述内容对应的是第 5(0x05)个常量池项。
test-class-9
回顾第 4、5 个常量池项的内容,我们可以得出该项实际表示的是 <init> ()V 也就是无参构造函数。

而 TestClass 中的最后两个常量池项(第 14、15 项)的类型都是 Utf8,表示的内容分别是 bbm/jvm/TestClassjava/lang/Object
test-class-10
通过上述的例子,想必大家已经对常量池的描述方式有了一定的了解,简单地总结一下就是:常量池中的每一项要么直接包含了所要描述的内容,比如 utf8 字符串,要么以编号的形式引用其他项的内容。在本例中,从第一个 Methodref 类型的常量池项开始,进一步地引用了诸如 Class,NameAndType,Utf8 等类型的常量池项。下图就描述了该 Methodref 项的整个引用关系。
test-class-reference
除了本例中出现了的 4 种常量池项类型外,还有 10 种不同的类型,它们的结构分别是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
CONSTANT_String_info {
u1 tag; // 类型编号 8
u2 string_index; // String 对应的 utf8 常量池项的编号
}

CONSTANT_Float_info {
u1 tag; // 类型编号 4
u4 bytes; // 值用 4 字节的无符号整形表示
}

CONSTANT_Double_info {
u1 tag; // 类型编号 6
u4 high_bytes; // 高地址的值用 4 字节的无符号整形表示
u4 low_bytes; // 低地址的值用 4 字节的无符号整形表示
}

CONSTANT_MethodType_info {
u1 tag; // 类型编号 16
u2 descriptor_index; // 描述符对应的 utf8 常量池项编号
}

CONSTANT_Fieldref_info {
u1 tag; // 类型编号 9
u2 class_index; // 字段所属类对应的 class 常量池项编号
u2 name_and_type_index; // 描述字段的对应的 name_and_type 常量池项编号
}

CONSTANT_InterfaceMethodref_info {
u1 tag; // 类型编号 11
u2 class_index; // 函数所属接口对应的 class 常量池项编号
u2 name_and_type_index; // 描述函数的对应的 name_and_type 常量池项编号
}

CONSTANT_Integer_info {
u1 tag; // 类型编号 3
u4 bytes; // 值用 4 字节的无符号整形表示
}

CONSTANT_Long_info {
u1 tag; // 类型编号 5
u4 high_bytes; // 高地址的值用 4 字节的无符号整形表示
u4 low_bytes; // 低地址的值用 4 字节的无符号整形表示
}

CONSTANT_MethodHandle_info {
u1 tag; // 类型编号 15
u1 reference_kind; // 方法句柄的类型用 1 字节的无符号整数表示
u2 reference_index; // 根据引用类型的不同可能会指向 Fieldref、Methodref、InterfaceMethodref 类型的常量池项
}

CONSTANT_InvokeDynamic_info {
u1 tag; // 类型编号 18
u2 bootstrap_method_attr_index; // 为指向引导方法表中的索引, 即定位到一个引导方法。引导方法用于在动态调用时进行运行时函数查找和绑定。引导方法表属于类文件的属性(Attribute)
u2 name_and_type_index; // 表示方法名字和签名的 NameAndType 常量池项的编号
}

这里我们单独解释一下 CONSTANT_MethodHandle_info 中的字段,我们知道 reference_kind 描述的是引用的类型,而 reference_index 会根据引用类型的不同指向不同类型的常量池项,它们之间的对应关系如下图所示。
method-handle-map

访问标记

在常量池后,紧跟着访问标记,该标记使用两个字节表示,用于表明该类的访问信息,如 public、final、abstract 等。每一种访问类型的表示都是通过设置访问标记的特定位来实现的。比如, 若是 public final 的类, 则该标记为 ACC_PUBLIC | ACC_FINAL
access-flag
在本例中访问标记的值为 0x21,因此可以判断出 TestClass 的类为 public,且 ACC_SUPER 标记置为 1。使用 ACC_SUPER 可以让类更准确地定位到父类的方法 super.method(), 现代编译器都会设置并且使用这个标记。
test-class-11

当前类、父类、接口

因为之前的例子比较简单,TestClass 中不包含任何字段和函数也没实现任何接口,为了通过该类解释这部分 Class 内容,所以我们现在对 TestClass 进行一定的修改,使其实现一些接口,包含成员变量和成员函数。修改完的 TestClass 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package bbm.jvm;

import java.io.Closeable;
import java.io.IOException;

public class TestClass implements Closeable, Cloneable {
private final boolean field1 = false;

@Override
public void close() throws IOException {
try {
boolean temp = false;
throw new IOException();
} catch (IOException e) {
System.out.println(e.toString());
}
}
}

修改完后,我们回顾一下描述当前类,父类,以及实现接口的格式,其中 this_class, super_class 都是 2 字节的无符号整数,它们指向常量池中的 CONSTANT_Class 项,因为 Java 中只能单继承,所以 super_class 只会指向一个 CONSTANT_Class 项。而一个类中可以实现多个接口,因此需要以数组的形式来保存多个接口的 CONSTANT_Class 项索引。

1
2
3
4
5
6
7
8
ClassFile {
...
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
...
}

回到我们的例子中,分析新的 TestClass 类文件,不难发现 this_class, super_class,指向的常量池项分别是 8(0x08) 和 9(0x09),而且因为 TestClass 实现了两个接口,所以 interfaces_count 为 2(0x02),这两个 interface 指向的常量池项分别是 10(0x0A) 和 11(0x0B)。这 4 个 Class 类型的常量池项分别引用了编号为 40-43(0x28-0x2B) 的 utf8 常量池项,而这四个 utf8 常量池项,分别对应了 bbm/jvm/TestClass, java/lang/Object, java/io/Closeable, java/lang/Cloneable
clazzs

字段

在接口描述后,就是类的字段信息,由于一个类会有多个字段,所以要先表明字段数,然后才是字段的描述。

1
2
3
4
5
6
ClassFile {
...
u2 fields_count;
field_info fields[fields_count];
...
}

字段的数量由一个 2 字节的无符号整数表示。而每个字段的具体信息由 field_info 结构描述,该结构内容如下:

1
2
3
4
5
6
7
field_info {
u2 access_flags; // 访问标记,例如:private public 等
u2 name_index; // 字段名称,指向 utf8 常量池项
u2 descriptor_index; // 字段的类型描述,指向 utf8 常量池项
u2 attributes_count; // 字段的属性数量
attribute_info attributes[attributes_count]; // 字段的属性可以用于存储恒量值等,比如 final 类型的字段会有一个 attribute 来描述恒量值
}

在 field_info 中,字段的访问标记非常类似于类的访问标记,该字段的取值参考如下对应表:
access-flag-field
下面我们以常量属性为例,来说一下字段的 attribute 数组中可能存储的内容:

1
2
3
4
5
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}

常量属性的 attribute_name_index 为 2 字节整数, 指向常量池的 CONSTANT_Ut8, 它指向的 utf8 常量池项只可能是如下这几种值,而当一个属性描述的是字段的恒量值时,所对应的字符串为“ConstantValue”。
attribute-name-utf8
接着, 为 attribute_length, 它由 4 个字节组成, 表示这个属性的剩余长度为多少。对常量属性而言, 这个值恒为 2。最后的 constantvalue_index 表示属性值, 但值并不直接出现在属性中, 而是存放在常量池中, 这里的 constantvalue_index 也是指向常量池的索引, 根据 field 类型的不同,constantvalue_index 可能会指向不同类型的常量池项,具体的对应关系如下图所示。
field-constant-attribute
接下来,回到我们的 TestClass 例子中,我们可以看出 TestClass 中存在 1(0x01) 个变量,该变量的访问标记为 0x12 因为该字段是 private final。变量名对应的编号为 12(0x0C)的 utf8 常量池项,该项的内容为 field1。字段的类型描述对应了编号为 13(0x0D)的 utf8 常量池项,该项的内容为 Z 表示字段的类型为 boolean。紧接着,该字段还有 1(0x01)个属性,该属性的名字指向了编号为 14(0x0E)的 utf8 常量池项,该项的内容为 ConstantValue,这表明该属性描述的是该字段的恒量值,所以 attribute_length 为 0x0002,并且 constantvalue_index 指向了编号为 15(0x0F)的 Integer 类型(0x03)常量池项,该项的内容为 0,也就是说 field1 的恒量值为 false
field-class

方法

在字段之后,就是类的方法信息,它和字段信息一样由两部分组成:

1
2
3
4
ClassFile {
u2 methods_count;
method_info methods[methods_count];
}

其中方法数量用 2 字节的无符号整数表示,接着就是这些函数的信息,每个 method_info 结构表示一个方法。

1
2
3
4
5
6
7
8
9
10
11
12
method_info {
u2 access_flags; // 访问标记,例如:private public 等
u2 name_index; // 函数名称,指向 utf8 常量池项
u2 descriptor_index; // 函数签名,指向 utf8 常量池项
u2 attributes_count; // 函数的属性数量
attribute_info attributes[attributes_count]; // 特属于该函数的属性,和字段属性的组织方式相同,只不过类型不同
}
attribute_info {
u2 attribute_name_index; // 属性的名字,指向 utf8 常量池项
u4 attribute_length; // 属性的长度
u1 info[attribute_length]; // 属性的内容,当属性类型不同时,info 的内容各不相同
}

在 method_info 结构中,访问标记(access_flags)和字段的访问标记非常类似,它们的取值如下表所示。
access-flag-method
紧接着是描述函数名和函数签名的信息,它们都是通过 2 字节的无符号数指向了对应的 utf8 常量池项编号。method_info 中,最后的就是属性部分,它和字段的属性组织方式基本一样,只不过函数使用的属性的类型和字段使用的属性的类型大不相同,方法的主要信息都存放在属性中。

在我们的例子 TestClass 中,我们可以看到它总共有 2(0x02) 个函数,其中第一个函数的 access_flags 为 PUBLIC(0x01),函数名存储在编号为 16(0x10)的常量池项中,内容为 <init> 也就是构造函数,函数的签名存储在编号为 17(0x11)的常量池项中,内容为 ()V。该方法有 1(0x01)个属性,该属性的名称存储在 18(0x12)号常量池项中,内容为 Code,说明该属性中保存了代码内容,并且整个 Code 属性的长度为 56(0x38) 字节。
method1-class
TestClass 中的第二个函数的 access_flags 也为 PUBLIC(0x01),函数名存储在编号为 23(0x17)的常量池项中,内容为 close,函数的签名存储在编号为 17(0x11)的常量池项中,内容为 ()V。该方法有 2(0x02) 个属性,第一个属性的名称存储在在 18(0x12)号常量池项中,内容为 Code,说明该属性中保存了代码内容,并且整个 Code 属性的长度为 120(0x78) 字节。第二个属性的名称存储在 29(0x1D)号常量池项中,内容为 Exceptions,该属性的长度为 4(0x04)字节。
method2-class

方法属性

Code 属性

在方法的属性当中最重要的属性当属 Code,它存放着函数的字节码信息,它的结构如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Code_attribute {
u2 attribute_name_index; // 指定了该属性的名称, 它是一个指向常量池的索引, 指向的类型为 CONSTANT_Utf8, 对于 Code 属性来说, 该值恒为 “Code”
u4 attribute_length; // 指定了 Code 属性的长度
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch type;
}
exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

在方法执行过程中, 操作数栈可能不停地变化, 但在整个执行过程中, 操作数栈存在一个最大深度, 该深度由 max_stack 表示。同理, 在方法执行过程中, 局部变量表也可能会不断变化。在整个执行过程中局部变量表的最大值由 max_locals 表示, 它们都是 2 字节的无符号整数。

在 max_locals 之后, 就是作为方法的最重要部分————字节码。它由 code_length 和 code[code_ength] 两部分组成, code_length 表示字节码的长度, 为 4 字节无符号整数, code[code_ength] 为 byte 数组, 为字节码内容本身。

在字节码之后, 存放该方法的异常处理表。异常处理表告诉一个方法该如何处理字节码中可能抛出的异常。异常处理表亦由两部分组成: 表项数量和内容。其中 exception_table_length 表示异常表的表项数量, exception_table[exception_table_length] 结构为异常表。表中每一行由 4 部分组成, 分别是 start_pc, end_pc, handler_pc 和 catch_type。这 4 项表示从方法字节码的 start_pc 偏移量开始到 end_pc 偏移量为止的这段代码中, 如果遇到了 catch_type 所指定的异常, 那么代码就跳转到 handler_pc 的位置执行。在这 4 项中, start_pc、end_pc 和 handler_pc 都是字节码的编译量, 也就是在 code[code_length] 中的位置, 而 catch_type 为指向常量池的索引, 它指向一个 CONSTANT_Class 类, 表示需要处理的异常类型。

至此, Code 属性的主体部分已经介绍完毕, 但是 Code 属性中还可能包舍更多信息, 比如行号、局部变量表等。这些信息都以 attribute 属性的形式内嵌在 Code 属性中, 即除了字段、方法和类文件可以内嵌属性外, 属性本身也可以内嵌其他属性。
method1-code
接下来,回到我们的例子 TestClass 中,我们先分析构造函数的 Code 属性部分,因为它更加简短,但是本质上和 close 函数是类似的。上图就展示了构造函数部分的 Code 属性内容(不包括 attribute_name_index 和 attribute_length)。可以看到构造函数的操作数栈最大深度为 2(0x02),最大局部变量数为 1(0x01),这里大家可能会有疑问怎么会有局部变量,这是因为在每个函数中 this 都会占用局部变量表的第一个位置。紧接着是字节码,它的长度为 10(0x0A), 内容为 2A B7 00 01 2A 03 B5 00 02 B1,在 JVM 中每个指令都有一个对应的字节码,构造函数的字节码翻译成指令后,内容如下:

1
2
3
4
5
6
0 aload_0 // 将第一个局部变量 this 推到操作栈顶
1 invokespecial #1 <java/lang/Object.<init>> // 执行父类 Object 的构造函数,执行结束后操作数栈清空,你会发现一个指令占1字节,一个引用占两个字节
4 aload_0 // 将第一个局部变量 this 推到操作栈顶
5 iconst_0 // 将 int 0 推到操作栈顶,注意此刻操作数栈中要包含 2 个数据(this 和 0),所以之前的操作数栈的最大深度是 2
6 putfield #2 <bbm/jvm/TestClass.field1> // 为 this 类实例的 field1 字段赋值 0
9 return // 当前函数返回 void

如果将上述字节码再转化为 Java 代码的话,大致的内容如下:

1
2
3
super();
this.field1 = false;
return;

在字节码之后的是异常表和属性表,可以看到 TestClass 的构造函数中异常表长度为 0(0x00),属性表长度为 2(0x02),第一个属性的名称存储在 19(0x13)号常量池项中(LineNumberTable),长度为 6(0x06),第二个属性的名称存储在 20(0x14)号常量池项中(LocalVariableTable),长度为 12(0x0C),这两个属性的具体内容我们后面再介绍。

close 函数的 Code 属性如下图所示(不包括 attribute_name_index 和 attribute_length)。
method2-code
可以看到 close 函数的操作数栈最大深度为 2(0x02),最大局部变量数为 2(0x01),这里大家可能会有疑问:函数中应该有 temp,this,和异常 e 这三个局部变量啊,为什么局部变量表的长度为 2 呢,实际上因为 temp 和 异常 e 处在不同的代码域中,所以它们会共用同一个局部变量槽位。紧接着是字节码,它的长度为 22(0x16), 翻译成指令后,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
 0 iconst_0 // 将 int 0 推到操作栈顶
1 istore_1 // 将栈顶 int 型值存入第二个局部变量中
2 new #3 <java/io/IOException> // 创建一个 IOException 对象,并将其引用压入操作数栈顶
5 dup // 复制栈顶数值(IOException 对象引用)并将复制值压入栈顶,至此操作数栈达到最大深度 2
6 invokespecial #4 <java/io/IOException.<init>> // 执行 IOException 的构造函数,执行后栈中只剩下一个 IOException 引用
9 athrow // 将栈顶的 IOException 异常抛出,执行该指令后,栈中本应为空,但是因为该异常又被 close 函数 catch 住了,所以 IOException 异常最后又会回到栈顶
10 astore_1 // 将栈顶 IOException 引用存入第二个本地变量
11 getstatic #5 <java/lang/System.out> // 获取 System 类的 out 静态属性,并将其压入栈顶
14 aload_1 // 将第二个局部变量 IOException 推送至栈顶
15 invokevirtual #6 <java/io/IOException.toString> // 执行栈顶 IOException 对象的 toString 函数,返回值会被推送至栈顶,此时栈内有两个数据(IOException 的 String,System.out)
18 invokevirtual #7 <java/io/PrintStream.println> // 执行 System.out 的 println 函数,参数是 IOException 的 String,该函数无返回值
21 return // 当前函数返回 void

在字节码之后的是异常表和属性表,可以看到 TestClass 的 close 函数中异常表长度为 1(0x01),start_pc = 0(0x00),end_pc = 10(0x0A),handler_pc = 10(0x0A),意为从字节码 0 偏移 到 10(不包含 10)偏移发生和第3(0x03)个常量池项匹配的异常后,应该调到偏移为 10 的字节码继续执行,而第 3 个常量池项是一个 Class 类型的项,它内部又指向了编号为 34(0x22)的 utf8 常量池项,内容为 java/io/IOException

异常表后的属性表长度为 3(0x03),第一个属性的名称存储在 19(0x13)号常量池项中(LineNumberTable),长度为 22(0x16),第二个属性的名称存储在 20(0x14)号常量池项中(LocalVariableTable),长度为 32(0x20),最后一个属性的名称存储在 27(0x1B)号常量池项中(StackMapTable),长度为 6(0x06)。

LineNumberTable 属性

Code 属性本身也包含着其他属性以进一步存储一些额外信息。首先, 来看一下 LineNumberTable, 它是 Code 属性的属性, 用于描述 Code 属性。LineNumberTable 用来记录字节码偏移量和行号的对应关系, 在软件调试时, 该属性有着至关重要的作用, 若没有它, 则调试器无法定位到对应的源码。LineNumberTable 属性的结构如下:

1
2
3
4
5
6
7
8
9
LineNumberTable_attribute {
u2 attribute_name_index; // 为指向常量池的索引, 在 LineNumberTable 属性中, 该值为"LineNumberTable"
u4 attribute_length; // 为 4 字节无符号整数, 表示属性的长度(不含前 6 个字节)
u2 line_number_table_length; // 表明了表项有多少条记录
{
u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length] // 表的实际内容
}

line_number_table 中包含 line_number_table_length 个 <start_pc, line_number> 元组, 其中, start_pc 为字节码偏移量, line_number 为对应的行号。
method2-line-number
在 TestClass close 函数的 LineNumberTable_attribute 中,有 5(0x05)个行号表项,起始字节码偏移到行号的映射关系如下,结合 close 函数的字节码和 Java 文件,不难看到它们的对应关系和 Class 文件中的内容完全一致。

  • PC:0(0x00) -> line: 12(0x0C) (为 temp 赋值)
  • PC:2(0x02)-> line: 13(0x0D) (抛出异常)
  • PC:10(0x0A)-> line: 14(0x0E) (catch 异常)
  • PC:11(0x0B)-> line: 15(0x0F) (打印异常)
  • PC:21 (0x15)-> line: 17(0x11) (函数结尾的括号)
LocalVariableTable 属性

对 Code 属性而言, 另外一个重要的属性是 LocalVariableTable, 也就是局部变量表, 它记录了一个方法中所有的局部变量, 它的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
LocalVariableTable_attribute {
u2 attribute_name_index; // 当前属性的名字, 它是指向常量池的索引。对局部变量表而言, 该值为"LocalVariableTable"
u4 attribute_length; // 属性的长度
u2 local_variable_table_length; // 局部变量表表项条目
{
u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length]
}

局部变量表的每一条记录由以下几个部分组成:

  • start_pc、length: 表示当前局部变量的开始位置(start_pc)和结束位置(start_pc + length, 不含最后一个字节)。
  • name_index: 局部变量的名称, 这是一个指向常量池的索引。
  • descriptor_index: 局部变量的类型描述, 指向常量池的索引。使用和字段描述符一样的方式描述局部变量。
  • index: 局部变量在当前帧栈的局部变量表中的槽位。对于 long 和 double 的数据, 它们会占据局部变量表中的两个槽位。

TestClass 的 close 函数的局部变量表信息如下所示,其中 0x14 就是 “LocalVariableTable” 局部变量项的编号 20,而局部变量表的长度为 32(0x20),局部变量表中总共包含 3(0x03)项。第一项的 start_pc = 2(0x02), length = 8(0x08), 变量名存储在 24(0x18)号常量池项中,内容为 temp,类型描述符存储在 13(0x0D)号常量池项中,内容为 Z,该项占用的局部变量表槽位为 1(0x01)。第二项的 start_pc = 11(0x0B),length = 10(0x0A),局部变量名存储在 25(0x19)号常量池项中,内容为 e,类型描述符存储在 26(0x1A)号常量池项中,内容为 Ljava/io/IOException,该项占用的局部变量表槽位为 1(0x01)。第三项的 start_pc = 0(0x00),length = 22(0x16),局部变量名存储在 21(0x15)号常量池项中,内容为 this,类型描述符存储在 22(0x16)号常量池项中,内容为 Lbbm/jvm/TestClass, 该项占用的局部变量表槽位为 0(0x00)。
method2-variable
总结一下,表中各项的内容如下:
method2-variable-table
可以看到,局部变量表中第 0 项一直是 this,而变量 temp 和异常 e 因为所处的执行域不同,所以它们共用了槽位 1,对照 close 函数的字节码后,你就会发现异常 e 的 start_pc = 11, 正好在 10 astore_1 // 将栈顶 IOException 引用存入第二个本地变量 之后。

StackMapTable 属性

对于 JDK1.6 以后的类文件, 每个方法的 Code 属性还可能含有一个 StackMapTable 的属性结构。该结构中存有若干个叫做栈映射帧(stack map frame)的数据。该属性不包含运行时所需的信息,仅用作 Class 文件的类型检验。StackMapTable 的结构如下:

1
2
3
4
5
6
StackMapTable_attribute {
u2 attribute_name_index; // 常量池索引,恒为 “StackMapTable”
u4 attribute_length; // 属性长度
u2 number_of_entries; // 栈映射帧的数量
stack_map_fram entries[number_of_entries]; // 具体的内容
}

每一个栈映射帧都是为了说明在一个特定的字节码偏移位置上, 系统的数据类型是什么(包括局部变量表的类型和操作数栈的类型)。每一帧都会显式或者隐式地指定一个字节码偏移量的变化值 offset_delta,使用 offset_delta 可以计算出这一帧数据的字节码偏移位置。计算方法就是将 offset delta+1 和上一帧的字节码偏移量相加。如果上一帧是方法的初始帧, 那么, 字节码偏移量为 offset_delta 本身。

这里说的“帧”, 和帧栈的帧不是同一个概念。这里更接近于一个跳转语句, 跳转语句将函数划分成不同的块, 每一块的概念就接近于这里所说的栈映射帧中的“帧”。

StackMapTable 结构中的 stack_map_frame 被定义为一个枚举值, 它可能的取值如下:

1
2
3
4
5
6
7
8
9
union stack_map_frame (
same_frame;
same_locals_1_stack_item_frame;
same_locals_1_stack_item_frame_extended;
chop_frame;
same_frame_extended;
append_frame;
full_frame;
}

第 1 个取值 same_frame 定义如下, 它表示当前代码所在位置和上一个比较位置的局部变量表是完全相同的, 并且操作数栈为空。它的取值为 0-63, 这个取值也是隐含的 offset_delta, 表示距离上一个帧块的偏移量。

1
2
3
same_frame {
u1 frame_type = SAME; /* 0-63 */
}

第 2 个取值 same_locals_1_stack_item_frame 的定义如下,其中, frame_type 的范围为 64-127, 如果栈映射帧为该值, 则表示当前帧和上一帧有相同的局部变量, 并且操作数栈中变量数量为 1。它有一个隐式的 offset_delta, 使用 frame_type-64 可以计算得来。之后的 verification_type_info, 就表示该操作数中的变量类型。

1
2
3
4
same_locals_1_stack_item_frame {
u1 frame_type = SAME_LOCALS_1_STACK_ITEM; /* 64-127 */
verification_type_info stack[1];
}

其中 verification_type_info 也是一个枚举值,它的可能取值如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
union verification_type_info {
Top_variable_info;
Integer_variable_info;
Float_variable_info;
Long_variable_info;
Double_variable_info;
Null_variable_info;
UninitializedThis_variable_info;
Object_variable_info;
Uninitialized_variable_info;
}

Top_variable_info {
u1 tag = ITEM_Top; // 0
}

Integer_variable_info {
u1 tag = ITEM_Integer; // 1
}

Float_variable_info {
u1 tag = ITEM_Float; // 2
}

Long_variable_info {
u1 tag = ITEM_Long; // 4
}

Double_variable_info {
u1 tag = ITEM_Double; // 3
}

Null_variable_info {
u1 tag = ITEM_Null; // 5
}

UninitializedThis_variable_info {
u1 tag = ITEM_UninitializedThis; // 6
}

Object_variable_info {
u1 tag = ITEM_Object; // 7
u2 cpool_index;
}

Uninitialized_variable_info {
u1 tag = ITEM_Uninitialized // 8
uoffset offset;
}

在我们的 TestClass 的 close 函数中,就包含一个该类型的 stack_map_frame。它保存在 close 函数的第三个属性中,该属性的名字指向 27(0x1B)号常量池项,内容为 “StackMapTable”,属性的长度为 6(0x06)。栈映射帧的数量为 1(0x01),帧类型为 74(0x4A)也就是刚才所说的 same_locals_1_stack_item_frame,同时隐含的 offset_delta = 74 - 64 = 10,那么 offset 为 0 的字节码是什么呢? 10 astore_1 // 将栈顶 IOException 引用存入第二个本地变量,在执行这条指令前栈内只有一份数据,也就是 IOException 对象引用,接下来我们看一下 StackMapTable 中描述的栈内容对不对。根据 verification_type_info 的 tag = 7(0x07)可知该校验类型为 Object,对应的 Object 保存在第 28(0x1C)号常量池项中,该项的类型是 Class 类型,Class 的名称保存在 34(0x22)号常量池项中,内容为 “java/io/IOException”。看来 StackMapTable 中描述的内容和字节码中的执行情况正好匹配,它确实能起到类型验证的作用。
method2-stack-map
到这里 TestClass 的 Code 属性就结束了,我们接下来回过头来继续介绍 stack_map_frame 的其他类型,虽然这些类型在例子中没有出现,但是记录在此也能便于以后查阅。

第 3 个取值 same_locals_1_stack_item_frame_extended, 和 same_locals_1_stack_item_frame 含义相同, 但是前者表示的 offset_delta 范围非常有限, 如果超出范围, 则需要使用 same_locals_1_stack_item_frame_extended, 它使用显式的 offset delta。同样, 在结构的最后, 存放着操作数桟的数据类型, 它的结构如下。

1
2
3
4
5
same_locals_1_stack_item_frame_extended {
u1 frame_type = SAME_LOCAL_1_STACK_ITEM_EXTENDED; /* 247 */
u2 offset_delta;
verification_type_info stack[1];
}

第 4 个取值 chop_frame 则表示操作数桟为空, 当前局部变量表比前一帧少 k 个局部变量。其中, k 为 251 - frame_type。它的结构如下:

1
2
3
4
chop_frame {
u1 frame_type = CHOP; /* 248-250 */
u2 offset_delta;
}

第 5 个取值 same_frame_extended 和 same_frame 含义一样, 表示局部变量信息和上一帧相同, 且操作数栈为空。但是 same_frame_extended 显示指定了 offset_delta, 可以表示更大的字节偏移量。它的结构如下:

1
2
3
4
same_frame_extended {
u1 frame_type = SAME_FRAME_EXTENDED; /* 251 */
u2 offset_delta;
}

第 6 个取值 append_frame 表示当前帧比上一帧多了 k 个局部变量, 且操作数栈为空。其中 k 为 frame_type - 251。在 append_frame 的最后, 还存放着增加的局部变量的类型。它的结构如下:

1
2
3
4
5
append_frame {
u1 frame_type = APPEND; /* 252-254 */
u2 offset_delta;
verification_type_info locals[frame_type - 251];
}

以上类型均只保存了前后两个帧中变化的部分, 因此可以减少数据的大小。但是, 如果以上结构都无法表达帧的信息时, 则可以使用第 7 种结构 full_frame。它不用来表示连续两个帧之间的差异, 而是将局部变量表和操作数栈都做了完整的记录。它的结构如下:

1
2
3
4
5
6
7
8
full_frame {
u1 frame_type = FULL_FRAME; /* 255 */
u2 offset_delta;
u2 number_of_locals;
verification_type_info locals[number_of_locals];
u2 number_of_stack_items;
verification_type_info stack[number_of_stack_items];
}

可以看到, 在 full_frame 中, 显示指定了 offset_delta, 完整记录了局部变量表的数量(number_of_locals)、局部变量表的数据类型(locals)、操作数栈的数量(number_of_stack_items)和操作数栈的类型(stack)。

至此,Code 属性的全部内容就已经介绍完了,为了让大家更加清晰地理解 Method 结构,Code 属性,以及 LineNumberTable,LocalVariableTable,StackMapTable 的关系,不妨看一看下面这个关系图,其中的箭头表示了它们之间的附属关系,即 LineNumberTable,LocalVariableTable,StackMapTable 属于 Code 属性,而 Code 属性又属于 Method 结构。
relationship-method-code-attributes

Exception 属性

除了 Code 属性外,每个方法都可以有一个 Exception 属性,用于保存该方法可能抛出的异常信息,也就是受验异常(checked exception)。该属性的结构如下:

1
2
3
4
5
6
Exception_attribute {
u2 attribute_name_index; // 指定了属性的名称, 它为指向常量池的索引, 恒为"Exceptions"
u4 attribute_length; // 属性长度
u2 number_of_exception; // 表示表项数最即可能拋出的异常个数
u2 exception_index_table[number_of_exception]; // 罗列了所有的异常, 每一项为指向常量池的索引, 对应的常量为 CONSTANT_Class, 为一个异常类。
}

Exceptions 与 Code 属性中的异常表不同。Exceptions 属性表示一个方法可能拋出的异常, 通常是由方法的 throws 关键字指定的。而 Code 属性中的异常表, 则是异常处理机制, 由 try-catch 语句生成。

在 TestClass 中, attribute_name_index 是 29(0x1D), 内容为 “Exceptions”, attribute_length 4(0x04), number_of_exceptions 为 1(0x01), 最后 exception_index_table 指向常量池的索引 3(0x03), 该 CONSTANT_Class 常量池项的名称也存储在常量池项中,对应的常量池项为 34(0x22),内容为 java/io/IOException
method2-exceptions

Class 文件属性

在方法之后,就是 Class 文件的属性部分了,这也是 Class 文件中最后的一部分内容。

1
2
3
4
5
ClassFile {
...
u2 attributes_count;
attribute_info attributes[attributes_count];
}

Class 的属性类型也有好几种,比如 SourceFile,BootstrapMethod,InnerClasses,Deprecated 等。但是在介绍这些内容之前,我打算为大家推荐一个字节码查看工具,它叫做 jclasslib, 我这里使用的是 IDEA 的插件版, 通过它能够一目了然的看到 Class 文件中的所有组成部分,如果大家以后打算分析 Class 文件,还是推荐大家使用这个工具,并不推荐人工对着 Class 二进制文件进行分析,说实话,挺费眼睛的 ^.^,当然,如果您偏爱命令行工具的话,javap 是一个不错的选择。工具虽然好用,但是我们前面的图例都是以二进制文件分析为主,之所以这么做是想加深大家对 Class 文件组织方式的认识。但是,看到这想必大家已经对它了然于心了,所以后续我们将改为以 jclasslib 图例为主进行介绍。
jclasslib

SourceFile 属性

SourceFile 属性是属于 Class 文件的属性。它用于描述当前这个 Class 文件是由哪个源代码文件编译得来的, 格式如下:

1
2
3
4
5
SourceFile_attribute {
u2 attribute_name_index; // 属性名,指向常量池的一个索引,这里恒为 "SourceFile"
u4 attribute_length; // 属性长度,对于 SourceFile 而言,恒为 2
u2 sourcefile_index; // 表示源代码文件名,指向常量池的一个索引
}

在我们的 TestClass 中,SourceFile_attribute 的内容就是 TestClass.java,它保存在一个 utf8 常量池项中。
source-file-class

BootstrapMethod 属性

为了支持 JDK1.7 中的 invokedynamic 指令, Java 虚拟机増加了 BootstrapMethods 属性, 它用于描述和保存引导方法。引导方法可以理解成是一个查找方法的方法, invokeDynamic 需要能够在运行时根据实际情况返回合适的方法调用, 而使用何种策略去查找所需要的方法, 是由引导方法决定的, 这里的 BootstrapMethods 属性就是用于找到调用的目标方法,注意,这里所说的引导方法和正常的方法有些不同,正常的方法都是描述一个方法体,方法体包含方法的声明以及方法的字节码,而引导方法更像是描述一次函数的调用,其中包括实际调用的引导函数的引用,以及调用时所用的参数,这和前面的 Code 属性有很大的不同。该属性是 Class 文件的属性, 属性结构如下:

1
2
3
4
5
6
7
8
9
10
BootStrapMethods_attribute {
u2 attribute_name_index; // 属性名称,指向常量池索引,这里为 “BootstrapMethods”
u4 attribute_length; // 4字节数字,表示属性长度(不含前六个字节)
u2 num_bootstrap_methods; // 表示引导方法的数量
{
u2 bootstrap_method_ref; // 指向常量池的常数,对应的常量池项类型为 CONSTANT_MethodHandle,用于指明实际的引导函数
u2 num_bootstrap_arguments; // 指明引导函数的参数个数
u2 bootstrap_arguments[num_bootstrap_arguments]; // 参数的内容
} bootstrap_methods[num_bootstrap_methods]; // num_bootstrap_methods 个引导方法
}

因为,我们的 TestClass 中并不包含 invokeDynamic,所以我们现在对其进行一波改写,让它能够覆盖接下来的各种属性,改写后 TestClass 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package bbm.jvm;

import java.io.Closeable;
import java.io.IOException;
import java.util.function.Consumer;

@Deprecated
public class TestClass implements Closeable, Cloneable {

@Override
public void close() throws IOException {
Consumer<Integer> consumer = i -> {};
consumer.accept(1);
new TestInner().test();
}

@Deprecated
public void deprecated() {

}

public static final class TestInner {
public void test() {
System.out.println('1');
}
}
}

改写完之后,我们编译 TestClass 并用 jclasslib 打开它的 Class 文件,就会发现 close 函数中已经出现了 invokedynamic 指令,它的参数保存在 2 号常量池项中。

1
2
3
4
5
6
7
8
9
10
11
 0 invokedynamic #2 <accept, BootstrapMethods #0>
5 astore_1
6 aload_1
7 iconst_1
8 invokestatic #3 <java/lang/Integer.valueOf>
11 invokeinterface #4 <java/util/function/Consumer.accept> count 2
16 new #5 <bbm/jvm/TestClass$Test>
19 dup
20 invokespecial #6 <bbm/jvm/TestClass$Test.<init>>
23 invokevirtual #7 <bbm/jvm/TestClass$Test.hh>
26 return

2 号常量池项的类型是 CONSTANT_InvokeDynamic_info,其中的内容如下,可以看到其中包含一个 CONSTANT_NameAndType 类型的常量池项和一个引导方法的编号(本例中是编号 0),而 CONSTANT_NameAndType 常量池项中 Name 部分保存的是 accept,Type 部分保存的是 ()Ljava/util/function/Consumer;。这两个数据后面 JVM 会用到,我们后面再说,这里接着去看一下引导方法 BootStrapMethods_attribute 中都保存了什么。
bootstrap-method
引导方法中只包含一份数据(编号为 0),实际使用到的引导方法引用保存在 40 号常量池项中,该项类型为 CONSTANT_MethodHandle,对应的函数是 java/lang/invoke/LambdaMetaFactory.metafactory, 使用的参数有 3 个,它们分别保存在 41(CONSTANT_MethodType),42(CONSTANT_MethodHandle),43(CONSTANT_MethodType) 号常量池项中。

这里,我们不妨看一下 LambdaMetaFactory.metafactory 这个引导函数是怎么实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class LambdaMetafactory {
// ...
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
// ...
}

看到这大家可能会有疑问,metafactory 有 6 个参数啊,但是我们在 BootStrapMethods_attribute 中只含有 3 个参数。实际上,前三个参数是 JVM 帮我们自动填充的:

  • MethodHandles.Lookup caller: 代表查找上下文与调用者的访问权限, 使用 invokedynamic 指令时, JVM 会自动自动填充这个参数
  • String invokedName: 要实现的方法的名字, 使用 invokedynamic 时, JVM 自动帮我们填充(填充内容来自常量池 InvokeDynamic.NameAndType.Name), 在这里 JVM 为我们填充 “accept”,因为我们的 lambda 表达式实际上要实现的函数是 Consumer.accept。
  • MethodType invokedType: 调用点期望的方法参数的类型和返回值的类型(方法签名)。使用 invokedynamic 指令时, JVM 会自动自动填充这个参数(填充内容来自常量池 InvokeDynamic.NameAndType.Type), 在这里整个 invokedynamic 执行的是一个调用点 CallSite,这个调用点没有参数,返回值类型为 Ljava/util/function/Consumer;,而 invokedType 描述的就是这个调用点 CallSite 的签名 ()Ljava/util/function/Consumer;。这里大家可能有点难理解为什么调用点的签名中没有参数,我们稍后解答这个问题。
  • MethodType samMethodType: 函数对象(即 accept 函数)将要实现的接口方法类型, 这里对应的是运行时函数签名, 值为引导方法属性中保存的第一个参数 (Ljava/lang/Object;)V,之所以不是 (Ljava/lang/Integer;)V 是因为 Java 编译器中泛型的实现是通过泛型信息擦除的方式实现的,即所有泛型 T 的签名都被改写为 Object, 而在使用这些对象的地方,编译器会为其添加类型转换。
  • MethodHandle implMethod: 一个直接方法句柄(DirectMethodHandle), 描述在调用(accept)时将被执行的具体实现方法 (包含适当的参数适配, 返回类型适配, 和在调用参数前附加上捕获的参数), 在这里为 bbm/jvm/TestClass.lambda$close0, 细心地同学会发现该函数实际上是编译器自动帮我们添加的函数,其中的内容就是我们在 lambda 表达式中写的内容。
    • bootstrap-method
  • MethodType instantiatedMethodType: 函数接口方法替换泛型为具体类型后的方法类型, 如果不使用泛型的话和 samMethodType 一样, 当使用泛型时,因为泛型的实现是通过类型擦除进行的,所以这里需要记录实际的签名,即 (Ljava/lang/Integer;)V,这才是 Consumer<Integer> 实例中 accept 函数的实际签名。

名词解释:

  • 方法句柄(Method Handler): 方法句柄很像一个方法指针, 或者代理。通过方法句柄, 就可以调用一个方法。在Class文件的常量池中, 有一项常量类型为 CONSTANT_MethodHandle, 这就是方法句柄。
  • 调用点(CallSite): 调用点是对方法句柄的封装, 通过调用点, 可以获得一个方法句柄进行函数调用。使用调用点可以增强方法句柄的表达能力, 比如对于可变调用点来说, 它绑定的方法句柄是可变的, 因此, 对同一个调用点而言, 其调用函数是可变的。
  • 启动方法(BootstrapMethods): 通过启动方法可以获得一个调用点, 获取调用点的目的是为了进行方法绑定和调用。
  • 方法类型 (Method Type): 用于描述方法的签名,比如方法的参数类型、返回值等。根据方法的类型, 可以查找到可用的方法句柄。

接下来我们说一说,为什么 invokedType 等于 ()Ljava/util/function/Consumer;。编译器在编译的时候, 会将 Lambda 表达式的表达式体 (lambda body)脱糖(desugar) 成一个方法,在我们的例子中这个方法就是 TestClass.lambda$close0,此方法的参数列表和返回类型和 lambda 表达式一致, 也就是 (Ljava/lang/Integer;)V,如果有捕获参数,脱糖的方法的参数可能会更多一些。这里可能有的同学不明白什么是捕捉参数,捕捉参数就是 lambda 表达式内对外层空间变量的引用。比如,如果我们将 lambda 改成如下形式,那么局部变量 test 就是一个捕捉参数。

1
2
TestClass test = new TestClass();
Consumer<Integer> consumer = i -> {test.deprecated();};

如果我们将 TestClass 中的 lambda 表达式改成上述形式的话,TestClass.lambda$close0 的函数签名也会跟着变化,变成 (Lbbm/jvm/TestClass;Ljava/lang/Integer;)V
lambda-close-1
此外,编译器还会产生一个 invokedynamic 调用,调用一个 call site。这个 call site 被调用时会返回 lambda 表达式的目标类型(functional interface)的一个实现类,在我们的例子中这个实现类就是 Consumer<Integer>。这个 call site 称为这个 lambda 表达式的 lambda factory。lambda factory 的 bootstrap 方法是一个标准方法,就是我们前面介绍的 lambda metafactory。这个 CallSite 在没有捕捉参数时,大家可以把它理解成如下形式, 所以 invokedType 签名才是 ()Ljava/util/function/Consumer;

1
Consumer<Integer> consumer = callSite();

而当存在捕捉参数时,就比如前面的 i -> {test.deprecated();};, 这时候 CallSite 的签名就会发生变化,变成 (Lbbm/jvm/TestClass)Ljava/util/function/Consumer;
invoke-type-2

InnerClasses 属性

InnerClass 属性是 Class 文件的属性,它用来描述当前 Class 文件中定义的或者所使用的所有内部类,以及该内部类所属的外部类是哪个。它的结构如下:

1
2
3
4
5
6
7
8
9
10
11
InnerClasses_attribute {
u2 attribute_name_index; // 属性名,指向常量池索引,这里恒为 "InnerClaaes"
u4 attribute_length; // 属性长度
u2 number_of_classes; // 使用的内部类的数量
{
u2 inner_class_info_index; // 指向内部类对应的 Class 类型常量池项,表示内部类的类型
u2 outter_class_info_index; // 指向外部类对应的 Class 类型常量池项,表示外部类的类型
u2 inner_name_index; // 表示内部类的名称,指向 utf8 常量池项
u2 inner_class_access_flags; // 内部类访问标示符,用于指示 public static 等,存储方式与之前的访问标示符相同
} classes[number_of_classes];
}

在我们的 TestClass 中,有一个 TestInner 内部类,它的 Class 文件保存在一个单独的 Class 文件中,与 TestClass 的 Class 文件是独立的。
inner-class-file
而在 TestClass 的 Class 文件中的 InnerClasses 属性,会记录所有在 TestClass 中定义或者使用到的内部类。因为 TestClass 中使用了 lambda 表达式,lambda 表达式的 bootstrap 是 metafactory, 而该函数的第一个参数的类型就是一个内部类 MethodHandles.Lookup。所以,在我们的 TestClass 的 Class 文件中 InnerClasses 属性有两项内容,一个是 TestInner, 另一个就是 MethodHandles.Lookup
inner-class

Deprecated 属性

Deprecated 属性可以出现在,类,方法,字段结构中,用于表示该类,字段,方法将来会被废弃,它的结构很简单:

1
2
3
4
Deprecated_attribute {
u2 attribute_name_index; // 属性名,指向常量池索引,这里恒为 "Deprecated"
u4 attribute_length; // 属性长度, 这里恒为 0
}

在 TestClass 中,因为整个类和类中的 deprecated 函数都被打上了 @Deprecated 注解,所以 Deprecated 属性即会出现在 deprecated 函数的属性中,也会出现在 TestClass 类的属性中。
deprecated

参考内容

[1]《实战 Java 虚拟机》
[2]《深入理解 Java 虚拟机》
[3] GC复制算法和标记-压缩算法的疑问
[4] Java中什么样的对象才能作为gc root,gc roots有哪些呢?
[5] concurrent-mark-and-sweep
[6] 关于 -XX:+CMSScavengeBeforeRemark,是否违背cms的设计初衷?
[7] Java Hotspot G1 GC的一些关键技术
[8] Java 垃圾回收权威指北
[9] [HotSpot VM] 请教G1算法的原理
[10] [HotSpot VM] 关于incremental update与SATB的一点理解
[11] Java线程的6种状态及切换
[12] Java 8 动态类型语言Lambda表达式实现原理分析
[13] Java 8 Lambda 揭秘
[14] 字节码增强技术探索
[15] 不可不说的Java“锁”事
[16] 死磕Synchronized底层实现–概论
[17] 死磕Synchronized底层实现–偏向锁
[18] 死磕Synchronized底层实现–轻量级锁
[19] 死磕Synchronized底层实现–重量级锁

贝克街的流浪猫 wechat
您的打赏将鼓励我继续分享!
  • 本文作者: 贝克街的流浪猫
  • 本文链接: https://www.beikejiedeliulangmao.top/java/jvm/class-file/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 创作声明: 本文基于上述所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。