04_jvm_04_class_file

第五十章 Java类文件结构(Structure of .class File)

1 概述

对于运行于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

2 .class文件结构

.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分别表示单字节、双字节、和四字节字段。

2.1 文件头

如下所示,.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_PUBLIC0x0001该类/接口为public。
ACC_FINAL0x0010该类定义为final,不能被继承。
ACC_SUPER0x0020其父类得方法需要特殊处理。
ACC_INTERFACE0x0200这是一个接口,不是类。
ACC_ABSTRACT0x0400这是一个抽象类,不能实例化。
ACC_SYNTHETIC0x1000这是一个合成类,不是从源代码中生成得类。
ACC_ANNOTATION0x2000这是一个标注(Annotation)。
ACC_ENUM0x4000这是一个枚举类型。
ACC_MODULE0x8000这是一个模块(Module)。

2.2 常量池(Constant Pool)

在常量池中,每一个元素表示一个常量。在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 (类类型)71.0.2
CONSTANT_Fieldref (成员变量引用类型)91.0.2
CONSTANT_Methodref (成员方法引用类型)101.0.2
CONSTANT_InterfaceMethodref (接口方法引用类型)111.0.2
CONSTANT_String (字符串类型)81.0.2
CONSTANT_Integer (整数类型)31.0.2
CONSTANT_Float (单浮点数类型)41.0.2
CONSTANT_Long (长整数类型)51.0.2
CONSTANT_Double (双浮点数类型)61.0.2
CONSTANT_NameAndType121.0.2
CONSTANT_Utf8 (UTF8编码的字符串)11.0.2
CONSTANT_MethodHandle157
CONSTANT_MethodType167
CONSTANT_Dynamic1711
CONSTANT_InvokeDynamic187
CONSTANT_Module (模块类型)199
CONSTANT_Package (包类型)209

因为篇幅所限,我们仅列出一些最为常用的常量结构。其他的常量类型可参考Java虚拟机标准文档的第4.4章The Constant Pool

2.2.1 类类型常量结构(CONSTANT_Class_info)

类类型常量结构(CONSTANT_Class_info)比较简单,它是由一个单字节tag和一个双字节name_index组成。tag的值为7。name_index为一个常量池数组的索引值。由该索引值可以从常量池中查找到一个CONSTANT_Utf8_info的结构体;该结构体包含着这个类或者接口的名字(这个名字是一个内部名字,由Java编译器生成)。

struct CONSTANT_Class_info {
  u1 tag;
  u2 name_index;
};

2.2.2 字符串类型常量结构(CONSTANT_String_info)

类似的,字符串类型常量结构(CONSTANT_String_info)也包含一个单字节tag和一个双字节string_index组成。tag的值为8;string_index指向常量池中一个CONSTANT_Utf8_info的结构体;该结构体包含着这个字符串的内容。

struct CONSTANT_String_info {
  u1 tag;
  u2 string_index;
};

2.2.3 整数类型和单精度浮点数常量结构(CONSTANT_Integer_info和CONSTANT_Float_info)

因为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;
};

2.2.4 长整数类型和双精度浮点数常量结构(CONSTANT_Long_info和CONSTANT_Double_info)

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;
};

2.2.5 Utf8编码字符串类型常量结构(CONSTANT_Utf8_info)

Utf8编码字符串类型结构包含了字符串的数据。除了单字节的tag以外,双字节的length表示字符串的长度;而bytes数组则包含了字符串的内容,其长度由length决定。

struct CONSTANT_Utf8_info {
  u1 tag; // 值为1
  u2 length;
  u1 bytes[length];
};

2.2.6 成员变量引用结构、成员方法引用结构、和接口方法引用结构

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;
};

2.2.7 CONSTANT_NameAndType_info结构

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;
};

2.3 成员变量(Fields)

在一个.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_PUBLIC0x0001public成员变量
ACC_PRIVATE0x0002private成员变量
ACC_PROTECTED0x0004protected成员变量
ACC_STATIC0x0008静态成员变量
ACC_FINAL0x0010成员常量
ACC_VOLATILE0x0040被声明为volatile的成员变量
ACC_TRANSIENT0x0080被声明为transient的成员变量
ACC_SYNTHETIC0x1000合成的成员变量(不在源代码中定义的成员变量)
ACC_ENUM0x4000枚举类型的一个成员

2.4 成员方法(Methods)

成员方法的结构与成员变量的结构非常相似。除了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_PUBLIC0x0001public成员方法
ACC_PRIVATE0x0002private成员方法
ACC_PROTECTED0x0004protected成员方法
ACC_STATIC0x0008静态成员方法
ACC_FINAL0x0010被声明为final的成员方法,不能被覆盖
ACC_SYNCHRONIZED0x0020被声明为syncrhonized的成员方法
ACC_BRIDGE0x0040桥方法,由Java编译器生成
ACC_VARARGS0x0080该成员方法的参数个数可变
ACC_NATIVE0x0100由其他编程语言实现的本地方法(Native Method)
ACC_ABSTRACT0x0400抽象方法,不能实例化
ACC_STRICT0x0800被声明为strictfp的方法(严格遵守IEEE浮点计算规范)
ACC_SYNTHETIC0x1000合成方法(未在源代码中定义的方法)

2.5 属性(Attributes)

所有的属性都是由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];
};

2.5.1 ConstantValue属性(ConstantValue Attribute)

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;
};

2.5.2 Code属性(Code Attribute)

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];
};

2.5.3 Signature属性(Signature Attribute)

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;
};

3 .class文件的格式检查与校验

当类加载器加载一个.class文件时,类加载器需要检查待加载的文件是否合法。这个格式检查的过程包括以下几个步骤。

  1. 前4字节的魔数必须匹配。
  2. 所有预定义的属性的长度需在合理的范围内。
  3. 待加载的文件必须完整;文件结尾处不能少数据,也不能多数据。
  4. 常量池中的元素必须与使用用途相匹配。例如:CONSTANT_Class_info结构中的name_index必须指向一个CONSTANT_Utf8_info的结构。
  5. 成员变量和成员方法必须包含合法的名字和描述符信息。

在.class文件加载之后,在链接阶段,Java虚拟机还需要进一步校验.class文件的内容。即使Java编译器能够严格遵守Java虚拟机标准,但是,这并不能保证在Java虚拟机上运行的.class文件都是由Java编译器生成的。因此,在运行代码之前,Java虚拟机需要做一些校验的工作。例如:Java虚拟机需要确保操作栈不会溢出、每条指令的操作数都是合法的。

Java虚拟机标准文档推荐了两种验证策略。

  1. 第一种策略是类型验证(Verification by Type Checking)。在这个过程中,Java虚拟机会根据Java语言的规则验证.class文件中包含的信息,已确保加载的类是类型安全的(Type-Safe)。这些验证包括:已声明为final的成员方法不能被覆盖;每个成员方法都有访问权限的属性(public/protected/private)等。
  2. 第二种策略是类型推断(Verification by Type Inference)。在这个过程中,Java虚拟机需要进行一些类型的推演。例如:Java虚拟机会检查局部变量是否在使用前已赋值;方法调用的参数都是合法的;变量类型与赋值的数值类型相匹配;所有的操作指令的参数类型都合法等。

4 Java虚拟机的限制

随着Java虚拟机运行环境的标准化,.class文件的格式也已确定。因为有些字段是固定长度的,这也给Java运行环境带来了许多限制。我们将这些限制列在下面,供读者参考。

  1. 在一个.class文件中,常量池最多能包含65535个元素,因为ClassFile.constant_pool_count是双字节数据。
  2. 一个类或者接口最多能定义65535个成员变量,因为ClassFile.fields_count是双字节数据。
  3. 一个类或者接口最多能定义65535个成员方法,因为ClassFile.methods_count是双字节数据。
  4. 一个类或者接口最多能实现或者继承65535个接口,因为ClassFile.interfaces_count是双字节数据。
  5. 在函数调用(Frame)中最多能定义65535个局部变量,因为Code_attribute.max_locals是双字节数据。
  6. 在函数调用(Frame)中操作栈的最大深度是65535,因为Code_attribute.max_stack是双字节数据。
  7. 一个函数最多可有255个参数,参考method_descriptor
  8. 成员变量、成员方法的名字和描述符的最大长度是65535个字符,因为CONSTANT_Utf8_info.length是双字节数据。
  9. 数组的最大维度是255,因为相关指令的参数是单字节数据(参见指令multianewarrayanewarraynewarray指令)。

5 总结

本章详细介绍了.class文件的结构以及各字段的含义。.class是Java编译器的输出,也是Java虚拟机的输入,在整个Java生态环境中处于最为核心的地位。.class文件也是Java运行环境向其他编程语言提供的标准接口。因为.class文件中字段的种类很多,我们仅挑选了一些重要的、有代表性的字段,其他的内容可参见Java虚拟机的标准文档

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.