Class文件结构

初始class文件

Java类文件是包含可在Java 虚拟机 (JVM)上执行的Java 字节码的文件(具有.class 文件扩展名)。Java 类文件通常由Java 编译器根据包含 Java 类的 Java 编程语言源文件(.java文件)生成(或者,其他JVM 语言也可用于创建类文件)。如果一个源文件有多个类,则每个类都被编译成一个单独的类文件。
Java虚拟机不包括Java语言在内的任何程序语言绑定,它只与"Class文件" 这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。

class文件出现的背景

Java在刚刚诞生之时曾经提出过一个非常著名的宣传口号 "一次编写,到处运行(Write Once,Run Anywhere)",这句话充分表达了当时软件开发人员对冲破平台界限的渴求。在每时每刻都充满竞争的IT业界,不可能只有Wintel(Windows与Intel的芯片相结合,曾是业界最强大的联盟)存在,我们也不希望出现只有Wintel而没有竞争者的世界,各种不同的硬件体系结构、各种不同的操作系统肯定将会长期并存发展。"与平台无关"的理想最终只有实现在操作系统以上的应用层:Oracle公司以及其他虚拟机发行商发布过许多可以运行在各种不同硬件平台和操作系统上的Java虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了层序的 "一次编写,到处运行"

Java虚拟机提供的语言无关性

Class文件结构_第1张图片

Class类文件的结构

Class文件是一组以字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全都是程序运行的必要数据,没有空隙存在。当遇到需要占用耽搁子节以上空间的数据项时,则会按照高位在前的方式分割成若干个子节进行存储。

根据《Java 虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:"无符号数""表"。后面的解析都要以这两种数据类型为基础,所以这里需要先明白这两个概念。

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引饮用、数值量或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合(引用)数据类型,为了便于区分,所有表的命名都习惯性地以 "_info" 结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表,这张表有以下所示的数据项按照严格顺序排列构成
类 型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u4 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
interfaces_info interfaces interfaces_count
u2 fields_count 1
fields_info fields fields_count
u2 methods_count 1
methods_info methods methods_count
u2 attributes_count 1
attributes_info attributes attributes_count
u2 fields fields_count
u2 fields fields_count

魔数与Class文件的版本

每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被Java虚拟机接受的Class文件。不仅是Class文件,很多文件格式标准中都有使用魔数来进行身份识别的习惯。

0xCAFEBABE(咖啡宝贝?)背景故事?

这个标识的由来是James Gosing在谈到帕洛阿尔托的一家餐厅时解释了这个神奇数字的历史:"我们过去常去一个叫圣米迦勒巷的地方吃午饭。根据当地传说,在黑暗的过去,Grateful Dead(1964年组建的一只美国乐队)在他们成名之前曾经在那里演出。这是一个非常时髦的地方,绝对是一个 Grateful Dead Kinda Place,Jerry(乐队的主唱)死后,他们甚至建了一个佛教风格的小神社,我们以前去那里的时候,我们称这个地方为 Cafe Dead。沿线的某个地方注意到这是一个 HEX 数字。我正在重新修改一些文件格式代码,需要一些神奇的数字:一个用于持久对象文件,一个用于类。我使用 CAFEDEAD 作为对象文件格式,并在grepping对于适合“CAFE”(这似乎是一个很好的主题)之后的 4 个字符的十六进制单词,我偶然发现了 BABE 并决定使用它。在那个时候,除了历史的垃圾桶之外,它似乎并没有什么特别的重要或注定要去任何地方。所以 CAFEBABE 成为类文件格式,而 CAFEDEAD 成为持久对象格式。但是持久对象工具消失了,随之而来的是 CAFEDEAD 的使用——它最终被RMI取代。"

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节都是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0 ~ 1.1使用了45.0 ~ 45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,因为《Java虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

为了说明以上魔数和版本,本人做了一段最简单的Java代码(如代码4.1),后面的内容都将以这段程序使用JDK9编译输出的Class文件为基础来进行演示。


package com.yzr.jvm.classf;

public class TestClass {

    private int a;

    public int increment() {

        return a + 1;
    }

}

图4.2显示的是使用十六进制编辑器 Sublime Text 打开这个类编译后的结果,可以清楚的看见开头四个字节的十六进制表示是0xCAFEBABE(两位十六进制代表8bit),代表次版本号的第5个和第6个字节值为0x0000,而主版本号的值为0x0034,也即是十进制的52(可通过在线转换工具进行进制转换:在线进制转换),该版本号说明这个是可以被JDK6或以上版本虚拟机执行的Class文件。

Class文件结构_第2张图片
图4.2

以下表格列出了从JDK1.1到13之间,主流JDK版本编译器输出的默认的和可支持的Class文件版本号。

JDK 版本 -target参数 -source参数 版本号 |
JDK 1.1.8 不支持target参数 不支持resource参数 45.3
JDK 1.2.2 不带(默认为-target 1.1) 1.1~1.2 45.3
JDK 1.2.2 -target 1.2 1.1~1.2 46.0
JDK 1.3.1_19 不带(默认为-target 1.1) 1.1 ~ 1.3 45.3
JDK 1.3.1_19 -target 1.3 1.1 ~ 1.3 47.3
JDK 1.4.2_10 不带(默认为-target 1.2) 1.1 ~ 1.4 46.0
JDK 1.4.2_10 -target 1.4 1.1 ~ 1.4 48.0
JDK 5.0_11 不带(默认为-target 1.1),后续版本不带target参数默认编译的Class文件均与其JDK版本相同 1.1 ~ 1.5 49.0
JDK 5.0_11 -target 1.4 -source 1.4 1.1 ~ 1.5 48.0
JDK 6 不带(默认为-target 6) 1.1 ~ 6 50.0
JDK 7 不带(默认为-target 7) 1.1 ~ 7 51.0
JDK 8 不带(默认为-target 8) 1.1 ~ 8 42.0
JDK 9 不带(默认为-target 9) 6 ~ 9 53.0
JDK 10 不带(默认为-target 10) 6 ~ 10 54.0
JDK 11 不带(默认为-target 11) 6 ~ 11 55.0
JDK 12 不带(默认为-target 12) 6 ~ 12 56.0
JDK 13 不带(默认为-target 13) 6 ~ 13 57.0

常量池

紧接着主、次版本之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项之一,另外,它还是Class文件中第一个出现的表类型数据项目。

由于常量池中的常量数量是不固定的,所以在常量池的入口需要放置一项u2(2字节)类型的数据,代表常量池容量计数值(constat_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始的,如下图4-3所示,常量池容量(便宜地址:0x000000008)为十六进制数0x0016,即十进制的22,这就代表常量池中有21项常量,索引值范围为1~21.在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达 "不引用任何一个常量池项目" 的含义,可以把索引值设置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般相同,是从0开始。

Class文件结构_第3张图片

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于Java语言层面的字面概念,如文本字符串、被声明为final的常量信息等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:

  • 被模块导出或者开放的包(Package)
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符
  • 方法句柄和方法类型(Method Handle、MethodType、Invoke Dynamic)
  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed-Constant)

Java代码在进行Javac编译的时候,在虚拟机加载Class文件的时候进行动态链接。也就是说,在Class文件中不保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不仅过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池中获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池中每一项常量都是一个表,截止JDK13,常量表中分别有17种不同类型的常量。这17类表都有一个共同特点,表结构其实的第一位是一个u1(1个字节)类型的标志位(tag,取值表见下表格中标志列),代表着当前常量属于哪种常量类型。17中常量类型具体含义如下:

类 型 标 志 描述
CONSTANT_Utf8_info 1 UTF-8 编码的字符串
CONSTANT_Integer 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 表示方法类型
CONSTANT_Dynamic_info 17 表示一个动态计算常量
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点
CONSTANT_Module_info 19 表示一个模块
CONSTANT_Package_info 20 表示一个模块中开发或者导出的包

请查看下图中的常量池的第一项常量,它的标志位是0x0a,通过查表的标志列可知这个常量是CONSTANT_Methodref_info类型,此类型的常量代表类方法的符号引用。CONSTANT_Methodref_info结构如下:

类型 名称 数量
u1 tag 1
u2 class_index 1
u2 name_index 1

tag是标志位,它用于区分常量类型;class_index是常量池的索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,此常量指向了声明方法的类描述符CONSTANT_Class_info的索引项本例中的class_index值为0x0004,也就是指向了常量池中的第二项常量。继续从上图中查找第二项常量,它的标志位是

访问标志

类索引、父类索引与接口索引集合

字段表集合

方法表集合

属性表集合

Code属性

待更新...

扩展知识

Grateful Dead

你可能感兴趣的:(Class文件结构)