对于运行于Java虚拟机之上的程序而言(例如:Java语言),.class文件是这些程序的二进制数据表达(Binary Representation)。.class文件包含着这些程序的全部数据和逻辑。对于Java虚拟机而言,.class文件是Java虚拟机的输入,它们驱动着Java虚拟机按照程序的逻辑运行。对于Java计算平台与运行环境而言,.class文件定义了一个统一的接口或者协议,遵守这个协议的程序都能在Java虚拟机上运行,提供了Java虚拟机的扩展功能。因此,在Java生态系统中,.class文件至关重要。
本章重点介绍.class文件的内部结构。在Java虚拟机标准文档中详细的描述了.class文件的结构,以及各个组成部分、各个字段的含义。.class文件是非常复杂的;因为篇幅所限,本文无法讲解每一个细节。因此,本文重点介绍.class文件中的重要结构和字段。其余的细节内容可参考Java虚拟机标准文档的第四章The class File Format。
.class文件是一个二进制文件。该文件需要按照读取字节流的方式顺序读取。在.class文件中,一个字段可能占用1个字节、2个字节、或者4个字节。当字段的宽度是2个字节或者4个字节时,该字段按照大端字节序(Big-endian Order)读取。大端字节序又常被称为网络字节序,或者网络序,即高位字节在低地址位置,低位字节在高地址位置(high byte comes first)。例如:如果顺序读取数值0x1234,则第一个字节的内容是0x12,第二个字节的内容是0x34。
因为.class文件是一个二进制文件,我们将借用C语言中的结构(struct)来表示.class文件中的结构。Java虚拟机标准文档将有些内容定义为数组(Array),而另一些定义为表(Table)。它们的区别在于表中的元素是变长的;而数组元素是固定长度的。在本文中,我们并不会特别区分这两者;我们会尽量使用平实的语言,将各部分内容描述清楚。
.class文件内的数据都是连续的,不存在填充数据(Padding)或者字节对齐(Alignment)问题。我们将遵循Java虚拟机标准文档中使用的符号,u1,u2,和u4分别表示单字节、双字节、和四字节字段。
如下所示,.class文件的文件头的结构非常清晰。前四个字节是魔数(Magic Number),固定为0xCAFEBABE。该魔数用于表示这是个.class文件。紧随其后的是双字节的次版本号(Minor Version)和主版本号(Major Version)。常量池信息包含在constant_pool_count和constant_pool里。constant_pool_count表示常量池的大小;constant_pool是一个数组,每个元素的类型是cp_info,用于表示一项常量信息。access_flags是一个双字节数据。它的每一位表示着该类的访问权限或者属性。其详细含义请参考表一。因为Class类型对象都是常量,所以,Class对象的名称是放在常量池中的。this_class和super_class两个字段指出了本类和父类的Class对象在常量池中的位置。interfaces_count和interfaces包含着该类实现的接口信息。在interfaces中,每个元素是一个双字节数据,用于指出其接口的Class对象在常量池中的位置。fields_count和fields包含了类成员变量信息;methods_count和methods包含了类成员方法信息。attributes_count和attributes包含了类属性信息。成员变量、成员方法、和类属性分别由field_info结构、method_info结构和attribute_info结构描述。
在接下来的内容中,我们重点介绍常量池、成员变量、成员方法和类属性的结构信息。
struct ClassFile {
u4 magic; // 魔数,0xCAFEBABE
u2 minor_version; // .class文件的版本号
u2 major_version;
u2 constant_pool_count; // 常量池的大小
cp_info constant_pool[constant_pool_count-1]; // 常量池
u2 access_flags; // 标识类或者接口的访问权限
u2 this_class; // 本类Class对象在常量池的序号
u2 super_class; // 父类Class对象在常量池的序号
u2 interfaces_count; // 实现接口的个数
u2 interfaces[interfaces_count]; // 接口Class对象在常量池的序号数组
u2 fields_count; // 成员变量的个数
field_info fields[fields_count]; // 成员变量信息
u2 methods_count; // 成员方法个数
method_info methods[methods_count]; // 成员方法信息
u2 attributes_count; // 属性个数
attribute_info attributes[attributes_count]; // 属性信息
};
表一、access_flags各个标志位的含义。
标识名称 | 值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 该类/接口为public。 |
ACC_FINAL | 0x0010 | 该类定义为final,不能被继承。 |
ACC_SUPER | 0x0020 | 其父类得方法需要特殊处理。 |
ACC_INTERFACE | 0x0200 | 这是一个接口,不是类。 |
ACC_ABSTRACT | 0x0400 | 这是一个抽象类,不能实例化。 |
ACC_SYNTHETIC | 0x1000 | 这是一个合成类,不是从源代码中生成得类。 |
ACC_ANNOTATION | 0x2000 | 这是一个标注(Annotation)。 |
ACC_ENUM | 0x4000 | 这是一个枚举类型。 |
ACC_MODULE | 0x8000 | 这是一个模块(Module)。 |
在常量池中,每一个元素表示一个常量。在Java 13版本的虚拟机中,常量池可包含17种常量类型,例如,我们熟知的常量类型有字符串类型(String)和类类型(Class)。所有的类型都由结构体cp_info表示。这个结构体是变长的,每一种类型的常量都由一个单字节tag开始。tag的取值标识着这个常量的类型。后续的info是该类型的数据。
struct cp_info {
u1 tag;
u1 info[];
};
我们将这17种常量类型对应的tag的取值和它的意义总结在表二中。"Java支持的版本"栏列出了支持该常量类型的最早版本。
表二、常量池结构体中tag取值的含义。
常量类型 | tag 取值 | Java支持的最早版本 |
---|---|---|
CONSTANT_Class (类类型) | 7 | 1.0.2 |
CONSTANT_Fieldref (成员变量引用类型) | 9 | 1.0.2 |
CONSTANT_Methodref (成员方法引用类型) | 10 | 1.0.2 |
CONSTANT_InterfaceMethodref (接口方法引用类型) | 11 | 1.0.2 |
CONSTANT_String (字符串类型) | 8 | 1.0.2 |
CONSTANT_Integer (整数类型) | 3 | 1.0.2 |
CONSTANT_Float (单浮点数类型) | 4 | 1.0.2 |
CONSTANT_Long (长整数类型) | 5 | 1.0.2 |
CONSTANT_Double (双浮点数类型) | 6 | 1.0.2 |
CONSTANT_NameAndType | 12 | 1.0.2 |
CONSTANT_Utf8 (UTF8编码的字符串) | 1 | 1.0.2 |
CONSTANT_MethodHandle | 15 | 7 |
CONSTANT_MethodType | 16 | 7 |
CONSTANT_Dynamic | 17 | 11 |
CONSTANT_InvokeDynamic | 18 | 7 |
CONSTANT_Module (模块类型) | 19 | 9 |
CONSTANT_Package (包类型) | 20 | 9 |
因为篇幅所限,我们仅列出一些最为常用的常量结构。其他的常量类型可参考Java虚拟机标准文档的第4.4章The Constant Pool。
类类型常量结构(CONSTANT_Class_info)比较简单,它是由一个单字节tag和一个双字节name_index组成。tag的值为7。name_index为一个常量池数组的索引值。由该索引值可以从常量池中查找到一个CONSTANT_Utf8_info的结构体;该结构体包含着这个类或者接口的名字(这个名字是一个内部名字,由Java编译器生成)。
struct CONSTANT_Class_info {
u1 tag;
u2 name_index;
};
类似的,字符串类型常量结构(CONSTANT_String_info)也包含一个单字节tag和一个双字节string_index组成。tag的值为8;string_index指向常量池中一个CONSTANT_Utf8_info的结构体;该结构体包含着这个字符串的内容。
struct CONSTANT_String_info {
u1 tag;
u2 string_index;
};
因为int类型的数值和单精度浮点数类型的数值的宽度都为4字节,所以,它们的结构相同,由一个单字节tag和一个四字节bytes组成,如下所示。单精度浮点数类型的bytes字段的值由单精度浮点数类型标准格式(IEEE 754 floating-point single precision format)决定。
struct CONSTANT_Integer_info {
u1 tag; // 值为3
u4 bytes;
};
struct CONSTANT_Float_info {
u1 tag; // 值为4
u4 bytes;
};
long类型的数值与双精度浮点数类型的数值的宽度为8字节,所以,它们需要使用两个四字节字段(high_bytes和low_bytes)来表示其取值,如下所示。双精度类型的high_bytes和low_bytes的取值遵守双精度浮点数类型标准格式(IEEE 754 floating-point double precision format)。
struct CONSTANT_Long_info {
u1 tag; // 值为5
u4 high_bytes;
u4 low_bytes;
};
struct CONSTANT_Float_info {
u1 tag; // 值为6
u4 high_bytes;
u4 low_bytes;
};
Utf8编码字符串类型结构包含了字符串的数据。除了单字节的tag以外,双字节的length表示字符串的长度;而bytes数组则包含了字符串的内容,其长度由length决定。
struct CONSTANT_Utf8_info {
u1 tag; // 值为1
u2 length;
u1 bytes[length];
};
CONSTANT_Fieldref_info, CONSTANT_Methodref_info, 和CONSTANT_InterfaceMethodref_info分别是成员变量引用、成员方法引用、和接口方法引用的结构。它们的结构非常相近。在tag之后,双字节class_index指向了常量数组中某一CONSTANT_Class_info类型的元素,双字节name_and_type_index则指向了常量数组中CONSTANT_NameAndType_info类型的元素。
因为Java语言的特点,成员变量引用和成员方法引用的class_index只能指向一个类,而不能指向一个接口(CONSTANT_Class_info可以表达类或者接口)。而接口方法引用的class_index只能指向一个接口,而不能指向一个类。
struct CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
};
struct CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
};
struct CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
};
CONSTANT_NameAndType_info结构用于表示一个成员变量或者成员方法。在这个结构中,并不需要指出该成员属于哪个类的信息。该结构定义如下。单字节tag字段取值为12。双字节name_index字段指向常量池中一个CONSTANT_Utf8_info元素,用于表示成员变量或者成员方法的名称。双字节descriptor_index也指向一个CONSTANT_Utf8_info元素,用于表示该成员的描述符信息(例如:成员变量的类型、成员方法的参数类型等)。
struct CONSTANT_NameAndType_info {
u1 tag; // 值为12
u2 name_index;
u2 descriptor_index;
};
在一个.class文件中,每个成员变量是由field_info结构来表达的。这个结构定义如下。双字节的access_flags表示该成员变量的访问权限与属性(见表三)。双字节的name_index和descriptor_index分别指向常量池中的CONSTANT_Utf8_info结构,用于表示该成员变量的名称和描述信息。双字节attributes_count表示属性的个数,而attributes数组则表示属性的内容。每个属性元素由attribute_info表示。该结构将在2.5小节详细介绍。
struct field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
};
表三、成员变量access_flags取值的含义。
名称 | tag 取值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | public成员变量 |
ACC_PRIVATE | 0x0002 | private成员变量 |
ACC_PROTECTED | 0x0004 | protected成员变量 |
ACC_STATIC | 0x0008 | 静态成员变量 |
ACC_FINAL | 0x0010 | 成员常量 |
ACC_VOLATILE | 0x0040 | 被声明为volatile的成员变量 |
ACC_TRANSIENT | 0x0080 | 被声明为transient的成员变量 |
ACC_SYNTHETIC | 0x1000 | 合成的成员变量(不在源代码中定义的成员变量) |
ACC_ENUM | 0x4000 | 枚举类型的一个成员 |
成员方法的结构与成员变量的结构非常相似。除了access_flags取值不同以外,其他字段表示的意义均相同。双字节的access_flags表示该成员方法的访问权限与属性(见表四)。双字节的name_index和descriptor_index分别指向常量池中的CONSTANT_Utf8_info结构,用于表示该成员方法的名称和描述信息。双字节attributes_count表示属性的个数,而attributes数组则表示属性的内容。每个属性元素由attribute_info表示。该结构将在下一小节详细介绍。
struct method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
};
表四、成员方法access_flags取值的含义。
名称 | tag 取值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | public成员方法 |
ACC_PRIVATE | 0x0002 | private成员方法 |
ACC_PROTECTED | 0x0004 | protected成员方法 |
ACC_STATIC | 0x0008 | 静态成员方法 |
ACC_FINAL | 0x0010 | 被声明为final的成员方法,不能被覆盖 |
ACC_SYNCHRONIZED | 0x0020 | 被声明为syncrhonized的成员方法 |
ACC_BRIDGE | 0x0040 | 桥方法,由Java编译器生成 |
ACC_VARARGS | 0x0080 | 该成员方法的参数个数可变 |
ACC_NATIVE | 0x0100 | 由其他编程语言实现的本地方法(Native Method) |
ACC_ABSTRACT | 0x0400 | 抽象方法,不能实例化 |
ACC_STRICT | 0x0800 | 被声明为strictfp的方法(严格遵守IEEE浮点计算规范) |
ACC_SYNTHETIC | 0x1000 | 合成方法(未在源代码中定义的方法) |
所有的属性都是由attribute_info结构表达的,定义如下。双字节attribute_name_index指向常量池中的CONSTANT_Utf8_info元素,用以表示属性的名称。四字节attribute_length和info表达该属性的内容。
Java 13版本共支持28中属性,我们挑选了一些重要的属性介绍,如果读者对其他属性感兴趣的话,可以参考Java虚拟机标准文档的第4.7章Attributes。
struct attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
};
ConstantValue属性是一种固定长度的属性,它用于成员变量结构field_info中,表达常量表达式的值(the value of a constant expression)。ConstantValue属性的结构除了包含attribute_name_index和attribute_length以外,constantvalue_index指向常量池中的一个元素。这个元素的类型必须与该常量的类型相匹配。例如,如果该常量是int, short, boolean类型的话,那么,constantvalue_index指向的常量池中元素的类型必须是CONSTANT_Integer。
struct ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
};
Code属性是一个变长的属性,它用于成员方法结构method_info中,表达方法的代码。Code属性结构体定义如下。max_stack指定该方法运行时的操作栈的最大深度(The Maximum Depth of the Operand Stack)。max_locals指定在调用该函数时局部变量的个数。code_length和code字段包含了在Java虚拟机上运行的代码。exception_table_length和exception_table包含了每一个异常处理程序(Exception Handler)的位置。start_pc和end_pc指明异常处理程序在code数组中的位置;handler_pc指明异常处理程序的第一条指令的位置;catch_type指向常量池中的一个CONSTANT_Class_info元素,用于表达待处理的异常类。attributes_count和attributes包含了这个Code属性的属性信息。
struct Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
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];
};
Signature属性是固定长度的,常用在成员变量field_info结构和成员方法method_info结构中,用于表示类、接口、构造函数、成员变量和成员方法的签名。Signature属性的结构定义如下,其中,signature_index指向常量池中的一个CONSTANT_Utf8_info元素,表示一个签名(签名是一个内部值,由Java编译器生成)。
struct Signature_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 signature_index;
};
当类加载器加载一个.class文件时,类加载器需要检查待加载的文件是否合法。这个格式检查的过程包括以下几个步骤。
在.class文件加载之后,在链接阶段,Java虚拟机还需要进一步校验.class文件的内容。即使Java编译器能够严格遵守Java虚拟机标准,但是,这并不能保证在Java虚拟机上运行的.class文件都是由Java编译器生成的。因此,在运行代码之前,Java虚拟机需要做一些校验的工作。例如:Java虚拟机需要确保操作栈不会溢出、每条指令的操作数都是合法的。
Java虚拟机标准文档推荐了两种验证策略。
随着Java虚拟机运行环境的标准化,.class文件的格式也已确定。因为有些字段是固定长度的,这也给Java运行环境带来了许多限制。我们将这些限制列在下面,供读者参考。
本章详细介绍了.class文件的结构以及各字段的含义。.class是Java编译器的输出,也是Java虚拟机的输入,在整个Java生态环境中处于最为核心的地位。.class文件也是Java运行环境向其他编程语言提供的标准接口。因为.class文件中字段的种类很多,我们仅挑选了一些重要的、有代表性的字段,其他的内容可参见Java虚拟机的标准文档。
注册用户登陆后可留言