Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
Class文件是由多个无符号数或者其他表作为数据项构成的复合数据类型。如下代码行所示描述了Class文件的内部结构。
ClassFile{
u4 magic;//Class文件的标志
u2 minor_version;//Class的小版本号
u2 major_version;//Class的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class文件的字段属性
field_info fields[fields_count];//一个类可以有多个字段
u2 methods_count;//Class文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
魔数
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件。程序设计者很多时候都喜欢用一些特殊的数字表示固定的文件类型或者其它特殊的含义。
Class文件版本号(Minor&Major Vesion)
紧接着魔数的四个字节存储的是Class文件的版本号:第5和第6位是次版本号,第7和第8位是主版本号。
每当Java发布大版本(比如Java 8,Java9)的时候,主版本号都会加1。你可以使用java-v命令来快速查看Class文件的版本号信息。
高版本的Java虚拟机可以执行低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行高版本编译器生成的Class文件。所以,我们在实际开发的时候要确保开发的的JDK版本和生产环境的JDK版本保持一致。
常量池
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。其中字符串常量池是运行时常量池中的一种,将会后续要在其他文章中补充。
静态常量池
所谓静态常量池,即*.class文件中的常量池(见上文Class文件内部结构),class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
引入常量池的原因是为了节约class文件的空间,举例如一个String类型参数,它继承Object类,它还有其他的方法,如果代码中全部引入,那么其代码文件大小是不可估量的。所以常量池通过记录引用的方式解决了空间问题。
这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等。
符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
- 类和接口的全限定名(全限定类名:就是类名全称,带包路径的用点隔开,例如:java.lang.String)
- 字段名称和描述符
- 方法名称和描述符
常量池中每一项常量都是一个表,这14种表有一个共同的特点:开始的第一位是一个u1类型的标志位-tag来标识常量的类型,代表当前这个常量属于哪种常量类型。
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。它由静态常量池通过JVM加载,在程序运行时存在。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
字符串常量池
在JVM中,为了减少字符串对象的重复创建,维护了一块特殊的内存空间,这块内存空间就被称为字符串常量池。在JDK1.6及之前,字符串常量池存放在方法区中。到JDK1.7之后,就从方法区中移除了,而存放在堆中。
访问标志
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否为public或者abstract类型,如果是类的话是否声明为final等等。
例如
package top.snailclimb.bean;
public class Employee{
...
}
通过javap-v class类名指令来看一下类的访问标志。
当前类、父类、接口、索引集合
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于Java语言的单继承,所以父类索引只有一个,除了java.lang.Object之外,所有的java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。
接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按implements(如果这个类本身是接口的话则是extends)后的接口顺序从左到右排列在接口索引集合中。
字段表集合
u2 fields_count;//Class文件的字段的个数
field_info fields[fields_count];//一个类会可以有个字段
其中field_info的结构如下所示:
字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
field info(字段表)的结构:
- access_flags:字段的作用域(public,private,protected修饰符),是实例变量还是类变量(static修饰符),可否被序列化(transient修饰符),可变性(final),可见性(volatile修饰符,是否强制从主内存读写)。
- name_index:对常量池的引用,表示的字段的名称;
- descriptor_index:对常量池的引用,表示字段和方法的描述符;
- attributes_count:一个字段还会拥有一些额外的属性,attributes_count存放属性的个数;
- attributes[attributes_count]:存放具体属性具体内容。
access_flag的取值:
方法表集合
u2 methods_count;//Class文件的方法的数量
method_info methods[methods_count];//一个类可以有个多个方法
methods_count表示方法的数量,而method_info表示方法表。Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
method_info(方法表的)结构:
方法表的access_flag取值:
注意:因为volatile修饰符和transient修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了synchronized、native、abstract等关键字修饰方法,所以也就多了这些关键字对应的标志。
属性表集合
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
在Class文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与Class文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。