内容简介
本节将讨论Java class文件的内容,包括常量池的结构和格式等,并详细描述其格式。该章节可作为Java class文件格式的全面参考。此外,通过对应案例分析Java虚拟机完整读取Java class文件的过程,有助于读者更好地理解本章所述内容。
class文件是什么
Java class文件是对Java程序二进制文件格式的精确定义。每个Java class文件都是对一个Java类或接口的全面描述。每个class文件只能包含一个类或接口。无论Java class文件在何种系统上产生,也无论虚拟机在何种系统上运行,Java class文件的精确定义使得所有的Java虚拟机都能正确读取和解释所有的Java class文件。
class文件与Java语言的关系
尽管class文件与Java语言结构相关,但是它并不一定必须与Java语言相关。例如,可以使用其他语言编写程序,然后将其编译为class文件,或者将Java程序编译为另一种不同的二进制文件格式。实际上,Java class文件的形式能够表示Java源代码无法表达的有效程序。然而,绝大多数Java开发者几乎都会选择使用class文件作为将程序传递给虚拟机的主要方式。
class文件的意义
"Class file"直译为类文件,因为着重阐述Java虚拟机的底层工作原理,因此有必要明确区分以下几个概念:
- “通常以.class为后缀名的Java虚拟机可装载文件”。
- .class后缀文件中包含的二进制流”。
- 在内存中形成的类或接口的映像。
“class文件”这个词特指“通常以.class为后缀名的Java虚拟机可装载文件”,其中可能包含类或接口的定义等信息。
Class文件内容分布规则
正如前面所提到的,Java的class文件是8位字节的二进制流,其中数据项按顺序存储在文件中,相邻的项之间没有任何间隔,从而使得class文件具有紧凑的特性。占据多个字节空间的项按照高位在前的顺序进行拆分,然后连续存放。
与Java类可以包含多个不同的字段、方法、方法参数、局部变量等类似,Java的class文件也可以包含多种不同大小的项。在class文件中,可变长度的项的大小和长度位于实际数据之前。这个特性使得class文件的流可以被顺序解析,首先读取项的大小,然后读取项的数据。
class文件的内容
Java的class文件包含了Java虚拟机所需的关于类或接口的所有信息。接下来会使用表格的形式来描述class文件的格式。在class文件中出现的项的有序列表。这些项按照它们在class文件中出现的顺序在表格中列出。
class文件之基本类型
在Java的class文件中,所有的多字节值(如u2、u4和u8)都以高位在前的形式出现,这也被称为大端字节序(Big-endian)。这意味着在class文件中,字节的排列顺序是从最高有效位到最低有效位。
每个项都包括类型、名称以及该项的数量。类型可以是表名,基本类型等。在class文件中,所有存储在u2、u4和u8类型的项中的值都以高位在前的形式出现。
基本类型 | 描述信息 |
u1 | 代表1个字节,无符号类型 |
u2 | 代表2个字节,无符号类型 |
u4 | 代表4个字节,无符号类型 |
u8 | 代表8个字节,无符号类型 |
例如,对于一个u2类型的值(2字节),高字节会出现在前面,低字节会出现在后面。类似地,对于一个u4类型的值(4字节),高字节会出现在前面,低字节会出现在后面。
这种字节序的约定是为了确保在不同平台上解析和处理class文件时的一致性。 不同的硬件和操作系统可能使用不同的字节序法(如小端字节序),因此在class文件中明确规定了大端字节序。
Class文件的格式
可变长度的Class文件表中的项在下表中展示,它们按照它们在class文件中出现的顺序列出了主要部分。
类型 | 名称 | 数量 |
u4 | magic | 1 |
u2 | minor version | 1 |
u2 | major_version | 1 |
u2 | constant pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
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 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
ClassFile表中各项信息
魔数头 (magic number)
每个Java class文件的**前4个字节(u4)**被称为它的魔数(magic number),通常为0xCAFEBABE。
解释:为什么是4个字节呢?0xCAFEBABE,其中0x代表着这个数值为16进制,CAFEBABE
是一共是8个16进制数,一个16进制数是4位,所以一共是4*8=32位的长度,故此为4个字 节。
魔术头的作用
魔数的作用在于轻松地区分Java class文件和非Java class文件。如果一个文件不是以0xCAFEBABE开头,那它就一定不是Java class文件。
魔术头的来源
之所以选择0xCAFEBABE这个值,是因为Java开发小组在寻找一个独特且易记的数值,而当时Java还被称为“Oak”。
根据Patrick Naughton(最初Java开发小组的关键成员)的说法,选择0xCAFEBABE只是一个巧合,它象征着一种咖啡品牌中的一种咖啡名字,这个名字预示了Java这个名字的出现。
minor_version(次版本)/ major_version(主版本)
随着Java技术的不断发展,Java class文件的格式可能会添加新的特性和发生变化,因此版本号也会相应地改变。
class文件的接下来的4个字节包含了主版本号和次版本号。
主、次版本的作用
对于Java虚拟机来说,版本号确定了特定的class文件格式,通常只有在给定的主版本号和一系列次版本号下,Java虚拟机才能正确地读取class文件。
如果class文件的版本号超过了Java虚拟机所能处理的有效范围,那么Java虚拟机将不会处理该class文件。
加载器所支持的版本范围
- 在Sun的JDK 1.0.2发布版中,Java虚拟机实现支持45.0到45.3的class文件格式。
- 所有JDK 1.1发布版中,虚拟机能够支持版本从45.0到45.65535的class文件格式。
- Sun的1.2版本的SDK中,虚拟机能够支持45.0到46.0的class文件格式。
编译器所支持的版本范围
在1.0或1.1版本的编译器中,可以生成版本号为45.3的class文件。在Sun的1.2版本SDK中,javac编译器默认生成版本号为45.3的class文件。但是,如果在javac命令行中指定了-target 1.2标志,1.2版本的编译器将生成版本号为46.0的class文件。需要注意的是,1.0或1.1版本的虚拟机无法class文件。
全局版本关系表
下面是Java版本的class的major_version之间的关系的表格(截止到java17):
Java版本 | 主发布版本号 | class的major_version范围 |
Java 1 | 45.0 | 45.0 - 45.3 |
Java 2 | 46.0 | 46.0 - 46.3 |
Java 3 | 47.0 | 0 - 47.3 |
Java 4 | 48.0 | 48.0 - 48.3 |
Java 5 | 49.0 | 49.0 - 49.3 |
Java 6 | 50.0 | 50.0 - 50.3 |
Java 7 | 51.0 | 51.0 - 51.3 |
Java 8 | 52.0 | 52.0 - 52.3 |
Java 9 | 53.0 | 53.0 - 53.3 |
Java 10 | 54.0 | 54.0 - 54.3 |
Java 11 | 55.0 | 55.0 - 55.3 |
Java 12 | 56.0 | 56.0 - 56.3 |
Java 13 | 57.0 | 57.0 - 57.3 |
Java 14 | 58.0 | 58.0 - 58.3 |
Java 15 | 59.0 | 59.0 - 59.3 |
Java 16 | 60.0 | 60.0 - 60.3 |
Java 17 | 61.0 | 61.0 - 61.3 |
注意,这里列出的是class的major_version范围,其中小数部分的.0表示次版本号,范围从0到3。这个表格概括了Java各个主要版本的class文件的主版本号范围。
Java虚拟机实现的第二版中修改了对class的文件主版本号和次版本号的解释,对于第二版而言,class文件的主版本号与Java平台主发布版的版本号保持一致(例如,在Java2平台发布版上,主版本号从45升至46),次版本号与特定主平台发布版的各个发布版相关。
因此,尽管不同的class文件格式可以由不同的版本号表示,但版本号不一样并不代表class文件格式不同。版本号不同的原因可能只是因为class文件由不同发布版本的Java平台产生,可能class文件的格式并没有改变。
constant_pool_count(常量池数量)和constant_pool(常量池)
在class文件中,魔数和版本号后面的是常量池。常量池包含了与文件中类和接口相关的常量,如文字字符串、final变量值、类名和方法名等。Java虚拟机将常量池组织为引用列表的形式,在实际列表constant_pool之前,是入口在列表中的计数constant_pool_count。
常量池中的许多引用都指向其他的常量池引用,而且class文件中紧随着常量池的许多元素也会指向常量池中的入口。
在整个class文件中,指示常量池入口在常量池列表中位置的整数索引都指向这些常量池入口。列表中的第一项索引值为1,第二项索引值为2,以此类推。尽管常量池列表中没有索引值为0的入口,但缺失的这一入口也被constant_pool_count计数在内。
例如,当constant_pool中有14项(索引值从1到14)时,constant_pool_countE的值为15。
在class文件中,每个常量池入口都以一个长度为1byte的标志开始,这个标志用于指示该位置的常量的类型。Java虚拟机在获取并解析这个标志之后,就能确定标志后面的常量的类型是什么。
常量池的引用
每个标志都有一个对应的表格,这个表格的名称是通过在标志名后面添加"info"后缀来生成的。例如,对应于CONSTANT_Class标志的表格名称是CONSTANT_Class_info,对应于CONSTANT_Utf8标志的表格名称存储着Unicode字符串的压缩形式。关于各种不同常量池入口的表格将在后面详细介绍。
常量池标志说明
入口类型 | 标志值 | 描述 |
CONSTANT_Utf8 | 1 | UTF8编码的Unicode字符串 |
CONSTANT_Integer | 3 | int类型字面值 |
CONSTANT_Float | 4 | float类型字面值 |
CONSTANT_Long | 5 | long类型字面值 |
CONSTANT_Double | 6 | double类型字面值 |
CONSTANT_Class | 7 | 对一个类或接口的符号引用 |
CONSTANT_String | 8 | String类型字面值 |
CONSTANT_Fieldref | 9 | 对一个字段的符号引用 |
CONSTANT_Methodref | 10 | 对一个类中声明的方法的符号引用 |
CONSTANT_InterfaceMethodref | 11 | 对一个接口中声明的方法的符号引用 |
CONSTANT_NameAndType | 12 | 对一个字段或方法的部分符号引用 |
在动态连接的Java程序中,常量池扮演着十分重要的角色。除了存储字面常量(或者说直接量值),常量池还可以容纳以下几种符号引用:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
以上三种可以统称为类以及面向对象的元数据了。
方法、字段以及类之间的关系链接
由于class文件不包含其内部组件最终内存布局的信息,因此类、字段和方法不能直接通过class文件中的字节码引用。Java虚拟机从常量池获取符号引用,然后在运行时解析引用项的实际地址。
- 字段:类或接口的实例变量或类变量。字段的描述符是一个指示字段类型的字符串。
- 方法:方法描述符也是一个字符串,该字符串指示方法的返回值和参数的数量、顺序和类型。
在运行时,Java虚拟机使用常量池中的全限定名、方法和字段的描述符,将当前类或接口中的代码与其他类或接口中的代码连接起来。
调用方法的字节码指令将一个符号引用的常量池索引传递给所调用的方法。关于在常量池中使用符号引用的过程,在后面有更详细的描述。
access_flags访问标识
紧接常量池后的两个字节称为access_flags,它展示了文件中定义的类或接口的多个信息。
- 访问标志指明文件中定义的是类还是接口;
- 访问标志还定义了在类或接口的声明中使用了哪种修饰符;
- 类和接口是抽象的还是公共的;
- 类的类型可以为final,而final类不可能是抽象的;
- 接口不能是final类型。
ClassFile表内access_flags项的标志位
标志名 | 值 | 设置的含义 | 设置名 |
ACC_PUBLIC | 0x0001 | public类型 | 类和接口 |
ACC_FINAL | 0x0010 | 类为final类型 | 只有类 |
ACC_SUPER | 0x0020 | 新型的invokespecial语义 | 类和接口 |
ACC_INTERFACE | 0x0200 | 接口类型,不是类类型 | 所有的接口 |
ACC_ABSTRACT | 0x0400 | abstract类型 | 所有的接口,部分类 |
ACC_SUPER 标志在Sun的旧版本Java 编译器中具有向后兼容性。
然而,在当前版本的 Sun Java 虚拟机中,invokespecial指令的语义比旧版本更为严格。
因此,所有新版本的编译器都必须将 ACC_SUPER 标志置位,并且所有新的 Java 虚拟机实现都必须实现更新而严格的 invokespecial 语义。即使在 Sun 的旧版本编译器中,将 ACC_SUPER 标志设为0时,即使设定了该标志,Sun 的旧版本虚拟机也将忽略它。
this_class(两个字节)
接下来的两个字节是用来表示this_class项,它是指对常量池的索引,在this_class位置的常量池入口必须是CONSTANT_Class_info表,该表由两个部分组成:标签和name_index。
- 标签:一个具有CONSTANT_Class值的常量。
- name_index:向常量池入口的位置,该位置包含了类或接口的全限定名的CONSTANT_Utf8_info表。
this_class的常量池索引
对于自身而言,this_class项是一个指向常量池的索引。
当Java虚拟机查找this_class位置的常量池入口时,它会发现一个通过将自身的标签设置为CONSTANT_Class来标识自身的项。Java虚拟机知道,在CONSTANT_Class_info入口中,标签之后总会有一个名为name_index的指向常量池的索引。
因此,虚拟机会在name_index位置查找常量池入口,在这个位置,虚拟机应该能找到一个包含了类或接口全限定名的CONSTANT_Utf8_info入口。
this_class使用常量池的示例
this_class提供了一个关于如何使用常量池的示例
super_class(两个字节)
在class文件中,super_class项出现在this_class项之后,占据两个字节的空间。这个两个字节的值表示常量池中的一个索引,指向super_class的全限定名在常量池中的位置。
super_class项表示当前类的父类,也就是继承关系中的上一级。通过该项,可以追溯到继承关系的父类。在class字节码的引用关系中,super_class项通过索引值指向常量池中的一个CONSTANT_Class_info结构,而CONSTANT_Class_info结构则存储了父类的全限定名。
由于Java程序中所有对象的基类都是java.lang.Object类,除了Object类外,常量池索引super_class对于所有的类都有效。对于Object类,super_class的值为0。对于接口,在常量池入口super_class位置的项为java.lang.Object。
总结起来,根据class文件中的super_class项,我们可以推导出当前类的继承关系以及父类的全限定名。请注意,这只是简化的描述,实际的class字节码结构更为复杂。对于更详细的绘图需求,建议使用专业的开发工具或在线工具来实现。
interfaces_count和interfaces
interfaces_count
接着super_class之后的是interfaces_count项。这个项代表了在文件中,该类直接实现或者由接口所扩展的父接口的数量。
interfaces
在这个计数值之后,是一个名为interfaces的数组,它包含了对每个由该类或者接口直接实现的父接口的常量池索引。
父接口常量
每个父接口使用常量池中的CONSTANT_Class_info结构来进行描述,该结构指向父接口的全限定名。
这个数组可以容纳那些直接出现在类声明的implements子句或接口声明的extends子句中的父接口。在数组中,超类与实现的接口按照它们在implements子句和extends子句中出现的顺序(从左到右)排列。
fields count和fields
在class文件中,interfaces之后紧接着描述的是该类或接口中声明的字段。
首先是一个名为fields_count的计数,它表示类变量和实例变量总数。在这个计数后面,是field_info表的序列,表的数量由fields_count指定。只有由类或接口声明的字段才会在fields列表中列出,没有从超类或父接口继承而来的字段会被排除在外。
然而,fields列表中可能包含一些没有在对应Java源文件中声明的字段,这是因为Java编译器可能在编译时向类或接口添加字段。比如,对于内部类的fields列表来说,在保持对外围类实例的引用时,Java编译器会为每个外围类实例添加实例变量。这些在fields列表中的字段在源代码中并没有声明,它们使用Synthetic属性标识。
methods_count和methods
在class文件中,紧接着fields后面的是对该类或接口中声明的方法的描述。
methods_count
首先是一个名为methods_count的计数,它表示该类或接口中所有方法的总数,采用双字节长度。
这个计数只包括显式定义在该类或接口中的方法,不包括从超类或父接口继承来的方法。
在methods_count之后是方法本身的描述。方法描述以method_info表的列表形式呈现,包含了与方法相关的信息,例如方法名和描述符(包括返回值类型和参数类型)。
methods
对于非抽象、非本地的方法,method_info表还会包含方法局部变量所需的栈空间长度、异常表、字节码序列,以及可选的行号和局部变量表。如果方法可能抛出已验证的异常,method_info表也会包括一个异常列表。
attributes_count和attributes
在class文件的最后部分是属性(attribute)的部分,它提供了关于类或接口在该文件中定义的属性的基本信息。属性部分以attributes_count开始,它表示后续attributes列表中attribute_info表的数量总和。
attribute_info
每个attribute_info表的第一项是指向常量池中CONSTANT_Utf8_info表的索引,该索引对应属性的名称。
attribute_info属性有很多种类。Java虚拟机规范定义了一些属性种类,但任何人都可以根据特定的规则创建自己的属性种类,并将它们放置在class文件中。Java虚拟机必须忽略无法识别的属性。
属性在class文件中的多个位置出现,而不仅仅在顶层的ClassFile表的attributes项中。在ClassFile表中出现的属性主要提供与文件中定义的类和接口相关的信息;
- 在field_info表中出现的属性主要提供与字段相关的信息;
- 在method_info表中出现的属性主要提供与方法相关的信息。
彩蛋案例
在这里,我向大家推荐一本关于JVM优化和调优的实战系列书籍,《深入浅出Java虚拟机 — JVM原理与实战》。这本书是最新出版的,内容涵盖了与我们当前工作和开发实例密切相关的技术和实战案例。通过学习这本书,我们可以深入了解Java虚拟机的原理,并通过实践掌握优化和调优的技巧。我诚挚地推荐这本书给大家,相信它将为我们的工作和技术发展带来巨大的收益。希望大家能够抽出时间多多学习一下这本宝贵的资料。
【
当当-点击链接
】【
京东-点击链接
】