序言
在 Java 中,我们所编写的代码都会被编译为 class 文件,然后再通过虚拟机编译为本地机器码,供操作系统执行。那 Java 当中的 Class 文件的结构有哪些,分别有什么作用。接下来一起看这篇文章。
Class 类文件的结构
Java 的跨平台性很大一部分原因就是 Java 程序编译后为 Class 文件,而 Class 文件与操作系统无关,其是直接面向 Java 虚拟机的,由 Java 虚拟机来进行编译为机器码,供操作系统调用。
这里先解释一下 Class 文件中会用到的两种数据类型:“无符号数”和“表”。后面的解析都要以这两种数据类型为基础,所以先解释这两个名词的概念:
- 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节、8个字节,无符号数可以用来描述数字、索引引用、数量值或字符串
- 表是由多个无符号数或其他表作为数据项构成复合数据类型,所有的表名都以 “_info” 结尾。
任何一个 Class 文化都对应着一个类或者接口的信息。它是一组以8个字节为基础单位的二进制流,各个数据项目都严格按照顺序紧凑排列,中间没有任何分隔符。其文件格式如下:
Class 文件结构中的无论是顺序还是数量都是严格规定的,不会因为程序的代码而改变。
魔数与版本号
每一个 Class 文件的头4个字节被称为魔数,它的唯一作用是确定这个文件是否是一个能够被虚拟机所接受的 Class 文件。使用魔数而不是文件扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动。
紧接着魔数的4个字节存储的是 Class 文件的版本号:第5个和第6个是次版本号,第7个和第8个是主版本号。由于不同版本的 JDK 所能执行的 Class 文件的版本号都是明确固定的,较高的 JDK 版本可以执行较低的 Class 文件,但是不能执行超过该 JDK 所能接受的 Class 文件的最大版本号。(Java 虚拟机判断版本号是以主版本号来计算的)
这里提供给大家一个网站:www.onlinehexeditor.com/。 可以用来在线查看 Class 文件的十六进制结果。
关于次版本号,在 JDK 12 之前均未使用,全部固定为零。
常量池
主、次版本号之后的就是常量池入口。常量池中的常量的数量是不固定的,所以在常量池的入库需要一个 u2 类型的数据,表示常量池容量的计数值。这个计数值是从1开始的而不是从0开始的。为什么从1开始而不是从0开始是有特殊考虑的,这样做的目的是,如果后面某些指向常量池的索引值的数据正在特定的情况下需要表达“不引用任何一个常量池项目”,可以把索引值设置为0来表示。
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于 Java 语言层面的常量的概念,比如文本字符串、被声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析翻译到具体的内存地址之中。
访问标志
常量池结束后,紧接着的2个字节代表访问标志(access_flags),这个标志用来识别一个接口或类的访问信息。包括:这个 Class 是类还是接口;是否定义为 public 类;是否定义为 abstract 类;如果是类,是否被 final 修饰;等等。具体的标志以及含义如下:
类索引、父类索引、接口索引集合
类索引和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定该类型的继承关系。类索引用于确定该类的全限定名,父类索引用于确定该类的父类的全限定名,接口索引集合则用于描述该类实现了哪些接口。
类索引、父类索引、接口索引集合都按顺序排列在访问标志之后,对于接口索引集合,入口的第一项 u2 类型的数据为接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面的索引表不再占用任何字节。
字段表集合
字段表集合用来描述接口或类中声明的变量。字段表集合是用来记录该类当中定义了哪些字段,这里的字段是指 Java 程序当中包括类变量以及实例变量(成员变量),但是不包括方法内部的局部变量。
字段可以包括的修饰符有字段的作用域(public、private、protected)、是实例变量还是类变量(static)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段的数据类型、字段名称。上述信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫做什么名字、被定义为什么类型都是无法固定的,只能引用常量池中的常量来描述。
字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本 Java 代码中不存在的字段,譬如在内部类中为了保持对外部类的访问,编译器就会自动添加指向外部类的实例。
字段表结构如上所示:
- access_flag:标记字段的修饰符,public、private、protected、static、final等
- name_index:表示字段的简单名称,简单名称就是指没有类型和参数修饰的方法或者字段名称
- descriptor_index:表示字段或方法的修饰符,是用来描述字段的数据类型、方法的参数列和返回值
方法表集合
如果理解了字段表集合的内容,那方法表的内容也会变的很简单,因为方法表的结构和字段表的结构如出一辙。
与字段表集合相对应地,如果父类方法没有在子类中被重写,方法表集合中就不会出现来自父类的方法信息。但同样地,有可能会出现由编译器自动添加的方法,最常见的便是类构造方法
<clint>()和实例构造器<init>()
方法中的 Java 代码在被编译器编译成字节码指令后,存放在方法属性表集合中一个名为 Code 的属性里面 。
属性表集合
属性表是 Class 文件中最具扩展性的数据项目,其所能包含的属性有很多,如下所示:
这里面属性较多,就不一一描述了,大家感觉去可以去百度一下。这里只说一下一个比较重要的属性:ConstantValue
ConstantValue 属性
ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值。只有被 static 关键的修饰的的变量才可以使用这项属性。对于非 static 类型的变量的赋值是在实例构造器<init>()方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>()或者使用ConstantValue 属性。目前 Oracle 公司实现的 Javac 编译器的选择是,如果同时使用 final 和 static 来修饰一个变量,并且这个变量的数据类型是基本类型或者String的话,就将生成ConstantValue 属性来进行初始化;如果这个变量没有被 final 修饰,或者并非基本类型及字符串,则将会选择<clinit>()方法中进行初始化。 大家不妨写段代码测试一下。
总结
到此为止,Class 文件的总体结构就介绍完毕了,这节内容的知识在工作当中用处不大,但是面试偶尔会问到。另外就是这个 ConstantValue 属性,大家一定要熟悉,并且进行测试。