JVM之类文件结构

本篇博客主要学习了JVM生成的Class文件结构。

字节码概要

Java设计之初有个非常著名的口号“一次编写,到处运行”,虽然如今有很多新语言都实现了这个特性(如Python,Go等),但在那个年代这个口号无疑带来了巨大的意义。那么是什么让Java实现了跨平台的能力呢?答案就在字节码。通过各种不同平台的Java虚拟机,编译得到的字节码文件能够处处运行。
Jvm编译Java代码时得到的字节码文件是以Class为单位的。Class文件是一组以8个字节为基础单位的二进制流,各个数据项严格按照顺序紧凑排列在一起。对于8个字节以上空间的数据项,Class会拆成若干个8个字节并按照高位在前的顺序排列,并且Class文件的字节序是大端序。Class文件使用两种伪结构来存储数据:

  • 无符号数:如u1、u2、u4、u8分别代表1、2、4、8个字节,可用来描述数字、引用、数量值或utf-8编码构成的字符串值。
  • 表:由多个无符号数或者其他表作为数据项构成的符合数据类型。为了方便辨认,表的后缀都是“_info”结尾。

Class文件的结构如下图所示:

图1.1 Class文件结构

这里简单介绍下各个数据项的含义:

  1. magic:值是固定的,0xCAFEBABE(咖啡宝贝)
  2. minor_version:次版本号
  3. major_version:主版本号
  4. constant_pool_count:该值-1为常量池数量
  5. constant_pool:常量池
  6. access_flags:掩码标识,表明访问权限和该类/接口的属性。
  7. this_class、super_class、interfaces_count和interfaces:分别表示类索引、父类索引和接口索引集合,并通过这三者确定当前Class的继承关系。
  8. fields_count和fields:表明了声明的变量,包括了类变量和实例变量。
  9. methods_count和methods:表明了声明的方法。
  10. attributes_count和attributes:属性表集合。

简单的代码示例

下面将使用一个简单的类来说明Java类文件机制。

1
2
3
4
5
6
7
8
9
package twd;

public class Test {
private int a;

public void sayHello() {
System.out.println("Hello World");
}
}

对该java类进行编译,得到对应的class文件,其16进制显示如下:

图2.1 16进制表示

详细说明

下面将结合示例代码和对应class文件进行各字段的详细说明。

magic

魔法值,长度为4字节,固定为cafe babe。

次版本号和主版本号

次版本号和主版本号的长度均为2字节,次版本号再Jdk1.2到JDK12之前均未被使用(均为0),主版本号则指明了相应的JDK大版本号。两者在该示例中的值分别为0x0000和0x0034(十进制为52,对应JDK8)。

常量池数量和常量池

紧接着的2字节为(常量池数量-1),该示例中常量池数量为0x0021-1,即有32项索引。注意,在设计之初就把索引0空出来是有特殊考虑的,如果后面某些指向常量池的索引值的数据在特定情况下
需要表达“不引用任何一个常量池项目”的含义, 可以把索引值设置为0来表示。

常量池中的常量可分为两大类型:

  • 字面量:如文本字符串、声明为final的值等。
  • 符号引用:属于编译原理方面的概念,又可继续往下细分
    • 被模块导出或开放的包:package
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符
    • 方法句柄和方法类型
    • 动态调用点和动态常量

当虚拟机进行类加载时,会从Class常量池获得对应的符号引用,并在类创建时或运行时解析、翻译到具体的内存地址。

常量池数量后为具体的常量。常量的格式由两部分组成:

  • u1类型的标志位:代表当前常量属于哪种常量类型
  • 特定的常量表结构:特定于不同常量类型的表结构
图3.1 Jdk8表结构

最初常量表中共有11种结构各不相同的表结构数据,Jdk8中共有14中常量池类型(如图3.1所示)。截止Jdk13,已经有了17种表结构。接下来仅介绍在示例代码中出现的常量表结构,如果想更详细地了解,可阅读Java虚拟机规范

第一个常量表结构的标志为0x0a,即为CONSTANT_Methodref_info,其格式为:

1
2
3
4
5
CONSTANT_Methodref_info {
u1 tag;
u2 class_index; // 指向常量池中对应CONSTANT_Class_info(必须是类)的索引
u2 name_and_type_index; // 指向常量池中对应CONSTANT_NameAndType(方法)的索引
}

示例中对应的class_index为0x0006(6),name_and_type_index为0x0013(19)。

第二个常量表结构的标志为0x09,即为CONSTANT_Fieldref,其格式为:

1
2
3
4
5
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index; // 指向常量池中对应CONSTANT_Class_info(类或接口)的索引
u2 name_and_type_index; // 指向常量池中对应CONSTANT_NameAndType(域)的索引
}

示例中对应的class_index为0x0014(20),name_and_type_index为0x0015(21)。

第三个常量表结构的标志为0x08,即为CONSTANT_String_info,其格式为:

1
2
3
4
CONSTANT_String_info {
u1 tag;
u2 string_index; // 指向常量池中对应CONSTANT_Utf8_info的索引
}

示例中对应的string_index为0x0016(22)。

后续的常量池不一一介绍,先直接通过javap输出对应的常量池信息。

图3.2 示例class的常量池

再介绍下之前没出现的几个常量表结构。其中第五个常量表结构为CONSTANT_Class_info。其格式为:

1
2
3
4
CONSTANT_Class_info {
u1 tag;
u2 name_index; // 指向常量池中对应CONSTANT_Utf8_info的索引,即类或接口的名字
}

可以看到,第五个常量的name_index为25,指向twd.Test这个类名。

第七个常量表类型为CONSTANT_Utf8_info,其格式为:

1
2
3
4
5
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length]; // 采用utf-8编码
}

再来看下第19个常量表类型,是CONSTANT_NameAndType_info,其格式为:

1
2
3
4
5
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index; // 指向对应的CONSTANT_Utf8_info的索引,表示字段名或方法名
u2 descriptor_index; // 指向对应的CONSTANT_Utf8_info的索引,表示对字段或方法的描述
}

该常量指的是类的构造函数<init>,()V表示无参且无返回值。

访问标志

接下来是2字节的访问标识,示例中值为0x0021。根据下面的访问标志定义可示例Class的标识为ACC_PUBLIC和ACC_SUPER。

图3.3 访问标志规范

索引

接下来是各种索引:类索引(this_class)、父类索引(super_class)和接口集。

类索引

其值为0x0005,指向twd.Test这个CONSTANT_Class_info。

父类索引

其值为0x0006,表示父类为java.lang.Object。

接口集

在这里接口数量为0x0000,所以无实现接口。

字段表

示例中字段表数量为0x0001,后续使用field_info表示每个字段。field_info的格式为:

1
2
3
4
5
6
7
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
图3.4 字段访问标志规范

这里对应的值为:

1
2
3
4
access_flags:0x0002,即ACC_PRIVATE
name_index :0x0007,即a
descriptor_index:0x0008,即I,为int的字段描述符
attributes_count:0x0000,无属性

显然该字段对应的是代码中的private int a

方法表

示例中方法表数量为0x0002,表示有两个方法。每个方法的格式与field_info一样。

图3.5 方法访问标志规范

第一个方法对应的值为:

1
2
3
4
access_flags:0x0001,即ACC_PUBLIC
name_index :0x0009,即<init>,即为构造方法。
descriptor_index:0x000a,即()V。
attributes_count:0x0001,有一个属性。

对于属性,其对应的格式为:

1
2
3
4
5
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}

该属性的attribute_name_index为0x000b,即常量池中第11个常量Code,其长度为0x0000002f,对应的内容如下:

图3.6 <init>的Code属性

属性表

属性表用来描述某些专有信息,可出现在类文件、字段表和方法表中。用户可自定义属性格式,虚拟机会在运行时忽略它不认识的属性。在Jdk8中,预定义了23个属性:

图3.7 Jdk8预定义的属性类型

比较常见的属性有Code(附在方法表中,主要表示方法执行代码的字节码形式)、Exceptions等,想要详细了解的可参考《深入理解Java虚拟机》的6.3.7。

参考资料