文章内容来自《深入理解JVM》和网络资料整理
一、Class文件组成内容
class文件是一组以8位字节为基础单位的二进制流,其与Java虚拟机指令集和符号表以及若干其他辅助信息相对应。
该设计有如下优点:
- 平台无关性,class文件可以运行在任意平台,无需考虑各个平台机器指令集不同的问题;
- 语言无关性,不论何种语言,只要生成的class文件格式符合JVM虚拟机规范即可。
注:如果遇到8位字节以上空间的数据,则会按照高位在前的方式分割成若干个8位字节进行存储(Big-Endian,具体是指最高位字节在地址最低位、最低位字节在地址最高位的顺序来存储数据,它是SPARC、PowerPC等处理器的默认多字节顺序,而x86等处理器则是使用了相反的 Little-Endian 顺序来存储数据)
二、Class文件数据结构
Class文件采用了类似C语言结构体的伪结构来存储数据,主要有以下几个特点:
- 由无符号数和表两种数据结构组成;
- 集合,用来描述同一类型但数量不定的多个数据,格式为 容量计数器 + 数据集合;
- 没有任何分割符号(每个字节代表的含义,长度,先后顺序都不允许改变)。
2.1 无符号数
定义:class文件基本的数据类型,用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表现形式:以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数。
2.2 表
组成:由无符号数或者其他表作为数据项构成的复合数据类型。
特征:以_info 结尾。
功能:用于描述有层次关系复合结构的数据。
整个Class文件本质上就是一张表
三、Class文件数据项
先来看一下Class文件的整体结构(也即Class文件中字节码的顺序):
ClassFile {
u4 magic; //魔数,固定值0xCAFEBABE
u2 minor_version; //次版本号
u2 major_version; //主版本号
u2 constant_pool_count; //常量的个数
cp_info constant_pool[constant_pool_count-1]; //具体的常量池内容
u2 access_flags; //访问标识
u2 this_class; //当前类索引
u2 super_class; //父类索引
u2 interfaces_count; //接口的个数
u2 interfaces[interfaces_count]; //具体的接口内容
u2 fields_count; //字段的个数
field_info fields[fields_count]; //具体的字段内容
u2 methods_count; //方法的个数
method_info methods[methods_count]; //具体的方法内容
u2 attributes_count; //属性的个数
attribute_info attributes[attributes_count]; //具体的属性内容
}
接下来按照Class文件中字节码的顺序来介绍数据项。
3.1 魔数
- 每个Class文件的头4个字节称为魔数(Magic Number);
- 唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件;
- Class文件魔数的值为0xCAFEBABE。如果一个文件不是以0xCAFEBABE开头,那它就肯定不是Java class文件。
3.2 主次版本号
魔数后4个字节,第5个和第6个字节是次版本号(Minor Version),第7个和第8个字节是主版本号(Major Version)。
Java版本号从45开始,每个大版本发布版本号 +1
虚拟机拒绝超过其版本号的Class文件
3.3 常量池
可以说常量池是Class文件的资源仓库,主要存放两类常量:字面量和符号引用。
- 字面量(Literal): 类似Java中的常量,如文本字符串,声明为final的常量值等;
- 符号引用(Symbolic References):包括了下面三类常量:
- 类和接口的全限定名(Full Qualified Name);
- 字段的名称和描述符(Descriptor);
- 方法的名称和描述符这三类常量。
常量池结构:容量计数器(u2类型) + 常量。
容量计数从1开始,第0项腾出来满足后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”的意思,这种情况就可以把索引值置为0来表示。
常且池中每一项常量都是一个表,这些表都有一个共同的特点,就是表开始的第一位是一个ul类型的标志位,代表当前这个常量属于哪种常量类型。
以CONSTANT_Class_info类型为例,上表中的tag用来区分常量类型,name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,代表了这个类(或者接口)的全限定名。
3.4 访问标志
常量池之后两个字节标识类的访问标志,用于识别一些类或者接口层次的访问信息,主要包括:
- 这个Class是类还是接口;
- 是否定义public;
- 是否定义abstract类型;
- 如果是类的话是否被声明为final等;
3.5 类索引、父类索引、接口索引
- 类索引、父类索引:都是一个u2类型的数据。它们会对应到常量池中的类描述符常量,通过常量中的索引值就可以找到类的全限定名字符串;
- 接口索引:接口索引集合是一组u2类型的数据的集合。第一项u2类型的数据为接口计数器,表示接口索引表的容量,如果该类没有实现任何接口,该计算器值为0。
Class文件中由这三项数据来确定类的继承关系。
3.6 字段表集合
在Java中一般通过如下几项描述一个字段:字段作用域(public、protected、private修饰符)、是类级别变量还是实例级别变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、可序列化与否(transient修饰符)、字段数据类型(基本类型、对象、数组)以及字段名称。在字段表中,变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但可能列出原本Java代码之中不存在的字段,譬如,在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
Java语言中字段是无法重载的,必须使用不同的名称,但是对于字节码来说,字段可以重名,只要字段的描述符不一致
- access_flags表示字段修饰符,与类的access_flags类似,并且都是一个u2的数据类型。其中可以设置的标志位和含义见下表;
- name_index和descriptor_index都是对常量池的引用;
- name_index代表字段的简单名称;
- descriptor_index代表字段和方法的描述符。
解释一下“简单名称”、“描述符’‘以及前面出现过多次的“全限定名”这三种特殊字符串的概念:
- 全限定名:将类全名中的“.”替换成“/”,并在最后添加一个“;”,表示全限定名结束;
- 简单名称:没有类型和参数的方法或者字段名称;
- 描述符:描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值;
- 描述符规则:
- 基本数据类型(byte、char、double、float、int、long、short、boolean)以及void都用一个大写字符来表示。
- 对象类型用字符L加对象的全限定名来表示
- 数组类型,每一纬度使用一个前置的“[”字符来描述,如定义为”java.lang.String[][]”,将被表示为”[[Ljava/lang/String”,一个整形数组”int[]”将被表示为”[I”
- 描述方法时,参数列表在前,返回值在后,且参数列表需要按顺序放在一组小括号之内。
3.7 方法表集合
Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,只是在访问标志和属性表集合的可选项中有所区别。
方法表的访问标志中没有ACC_VOLATILE 和 ACC_TRANSIENT 标志,增加了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP、ACC_ABSTRACT 标志。
如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息,但有可能出现由编译器自动添加的方法,如类构造器
<clinit>
方法和实例构造器<init>
方法
3.8 属性表集合
Class文件、字段表、方法表都可以有自己的属性表集合,用于描述某些场景的专有信息。属性表集合的限制更宽松一些,不要求各个属性表具有严格顺序,并且只要不与已有属性名重复即可。
接下来对其中一些属性中的关键常用的部分进行介绍。
3.8.1 Code属性
最常用的属性恐怕就是Code属性了,因为大多数的方法都会有编译后的字节码指令,这些指令就存储在方法表中的Code属性中。如果一个Java程序的信息可以分为代码(方法体中的代码)和元数据(包括类、字段、方法定义以及其它信息),那么Code属性存储的就是代码,其它所有的结构存储的都是元数据。不过并非所有的方法表都有这个Code属性,比如接口或抽象类中的方法表就不存在Code属性(JDK 1.8中的接口也可以定义方法了)。Code属性的结构如下:
其中attribute_name_index和attribute_length前面已经介绍过了。
max_stack代表了操作数栈的最大深度。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机执行时需要根据这个值来分配栈帧中的操作栈深度。
max_locals代表了局部变量表所需要的存储空间。在这里,max_locals的单位是slot,在之前的文章中了解了HotSpot虚拟机在分配对象时使用的单位就是slot。方法参数(包括隐式参数this)、显式异常处理器的参数(try-catch块中catch块中定义的异常)以及方法体中定义的局部变量都需要局部变量表来存放。需要注意的是,由于局部变量表中的slot可以重用,所以并不是所有的局部变量的总slot就是max_locals。编译器会根据变量的作用域来分配slot给各个变量使用,然后计算max_locals的大小。
code_length和code用来存储字节码指令。Java的字节码指令的长度都是一个字节,即最多可以有256个指令,实际上一共有大约200条指令。