首页 > 代码库 > Class类文件的结构
Class类文件的结构
Class类文件的结构
Class类文件的结构
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反之类和接口并不一定定义在文件里(比如类和接口也可以通过类加载器直接生成)。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中,中间没有任何分隔符。Class文件的结构只有两种数据类型:无符号数和表。
- 无符号数以u1、u2、u4和u8来代表1个字节、两个字节、四个字节和八个字节的无符号数,可以用来描述数字、索引引用、数值量或者按UTF-8编码的字符串值。
- 表是有多个无符号数或者其他表作为数据项构成的复合数据类型,习惯性以“ _info ”结尾。整个Class文件实质上就是一张表。
下面是Class文件的格式:
魔数(magic)与Class文件的版本
每个Class文件的头四个字节称为魔数,它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件。class文件的魔数值为CA FE BA BE。
紧挨着魔数后面的四个字节存储的是Class文件的版本号:第五和第六是次版本号(minor_version),第七和第八是主版本号(major_version),以下面的类为例:
public class TestClass{
private String m;
public String test() {
return m + 1;
}
}
生成的Java Class文件结构为:
可以看到代表次版本号的第五个和第六个字节为0x0000,主版本号为0x0032,也就是十进制的50。
常量池
紧接着主版本号之后是常量池容量计数值(constant_pool_count),由于常量池中常量的数量不是固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count),这个常量技术值是从1开始而不是从0开始的。上图中常量池容量为0x0016,即十进制中的22,这就表示常量池中有21项常量。
然后就是常量池(constant_pool),常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是在Class文件中第一个出现的表类型数据项目。
常量池中主要存放两大类常量:字面量和符号引用。
- 字面量近似于Java的常量,如文本字符串、声明为final的常量值。
- 而符号引用则属于编译原理方面的概念,包括:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
Java代码在进行javac编译时没有连接的步骤,而是在虚拟机加载Class文件的时候进行动态连接。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
个人理解符号引用的作用就是在编译时记录下文件的类、字段和方法,在JVM运行时能在需要的时候获取相应信息进行加载。
在常量池中会有一部分自动生成的常量(这些常量没有在Java代码里面直接出现过),但这些常量会被字段表、方法表、属性表引用,用来描述一些不方便使用“固定字节”表达的内容。(比如:方法的返回值?有几个参数?参数类型?等)
访问标志
在常量池结束之后,有两个字节代表访问标志(access_flags),这个标志用于识别一些类和接口层次的访问信息,包括这个Class是类还是接口,是否为public类型,是否定义为abstract类型,如果是类的话,是否声明为final类型等。
类索引、父类索引和接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(包括了interfaces_count及interfaces)是一组u2类型的数据的集合,Class文件中由这个三个数据来确定类的继承关系。它们按顺序排在访问标志后面。
类索引用于确定这个类的全限定名,
父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继续,所以父类索引只有一个(除了java.lang.Object外所有的Java类都有父类,除了它所有的父类索引都不为零)。
接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按照Implements语句后的接口顺序从左到右排列在索引集合中。
字段表集合
字段表(field_info)用于描述接口或类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。可以包括的信息有:字段的作用域、是实例变量还是类变量(static 修饰符)、可变性(final)、并发可见性(volatile修饰)、是否可被序列化(transient修饰符)、字段数据类型、字段名称。上述的这些信息,各个修饰符都是布尔值。
字段叫什么名字、字段被定义为什么类型,这些是无法固定的,只能引用常量池中的常量来描述。
- 全限定名:"org/fenixsoft/clazz/TestClass"是这个类的全限定名,只是把类中的“.”换为了"/".
- 简单名称:没有类型和参数修饰的方法或字段名称。
- 描述符:描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
描述符描述方法:
- 对于数组类型,每一位都将使用一个前置的"["字符来描述,“[[Ljava/lang/String;”表示String[][]。
- 描述方法时采用先参数列表后返回值顺序描述,"([CII[CIII)I"来描述int indexOf(char[],int ,int ,char[] ,int ,int ,int )"
字段表集合中不会列出从超类或者父接口中继承而来的字段,但可能列出Java代码中不存在的字段,如内部类中保持对外部类的访问性,会自动添加指向外部类实例的字段;
方法表集合
方法里的Java代码经过编译器编译成字节码指令后,存放在属性表集合中一个名叫“Code”的属性里面。
如果父类方法在子类中没有被重写,那么方法表集合中不会出现来自父类的方法;同时也同样可能出现编译器自动添加的方法,典型如“类构造器< clinit >”和实例构造器"< init >"
如果要重载一个方法除了要与原方法具有相同的简单名称之外,还必须要求拥有一个与原方法不同的特征签名,即方法中各个参数在常量池中的字段符号引用集合,不包含返回值。这就是Java语言里仅仅依靠返回值不同无法对一个已有方法重载。但是在Class文件格式中即字节码层面(前面是Java代码层面),方法特征还包括方法返回值及受查异常表,因此2个不是完全一致的方法也可以合法共存与一个Class文件中。
属性表集合
- 在Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。
- 属性表要求稍微宽松,不再要求各个属性表具有严格执行顺序,只要不与现有属性名不重复即可。
以下是虚拟机规范定义的属性:
Code属性
Code属性:Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合中,但是并非所有的方法表都必须存在这个属性,例如接口或者抽象类中的方法就不存在Code属性。其属性表结构如下:
- attribute_name_index:指向CONSTANT_Utf8_info型常量的索引,固定值为“Code”代表该属性的属性名称。
- attribute_length:属性值的长度(即属性表的长减去6个字节,这六个字节为attribute_name_index及attribute_length)
- max_stack:操作数栈深度的最大值(JVM运行时根据这个值来分配操作栈深度)
- max_locals:局部变量表所需的储存空间(单位Slot)。对于byte,char,float,int,shot,boolean,reference和returnAddress等长度不超过32位的数据类型,每个局部变量占1个Slot,而double与long这两种64位的数据类型而需要2个Slot来存放。编译器会根据变量的作用域来分类Slot并分配给各个变量使用,然后计算出max_locals的大小。
- code_length、code:用来存储Java源程序编译后生成的字节码指令。
Exceptions属性
Exceptions属性:其作用是列举出可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。
- number_of_exceptions:表示可能抛出number_of_exceptions种受检查异常,每一种受检查异常使用一个exception_index_table项表示。
- exception_index_table:指向常量池中CONSTANT_Class_info型常量表的索引,代表了该受检查异常的类型。
LineNumberTable属性
LineNumberTable属性用于描述Java源代码行号与字节码行号(字节码偏移量)之间的对应关系。如果选择不生成LineNumberTable属性表,在抛出异常时堆栈中将不会显示出错的行号,也无法在调试程序的时候按照源码来设置断点。
LocalVariableTable属性
LocalVariableTable属性表用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。非必须。如果没有生成这项属性,IDE可能会使用诸如arg0、arg1之类的占位符来替换原有的参数名称,对程序运行没有影响。
SourceFile属性
SourceFile属性用于记录这生成这个Class文件的源码文件名称。这个属性也是可选的,如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错误代码所属的文件名。
ConstantValue属性
作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可以用这个属性。
- 对于非static类型的变量(实例变量)的赋值是在实例构造器< init >方法中进行的。
- 而对于类变量(static变量)有两种方式:在类构造器< clinit >方法中或者使用ConstantValue属性。
目前Sun javac编译器的选择是:同时使用final和static修饰的变量且为基本数据类型或String类型使用ConstantValue属性初始化,否则使用类构造器< clinit >进行初始化。
InnerClasses属性
用于记录内部类与宿主类之间的关联。
Inneclasses属性结构:
inner_classes_info表的结构:
Deprecated及Synthetic属性
Deprecated及Synthetic属性都属性于标志类型的布尔值属性,只存在有和没有的区别,没有属性值的概念。所以在下图属性结构中attribute_length的数据值必须为0x00000000。
- Deprecated属性用于表示某个类,字段或方法,已经被程序作者定为不再推荐使用,它可以通过代码中使用@Deprecated注解进行设置。
- Synthetic属代表此字段或方法并不是由Java源码直接产生的,而是由编译器自行添加的。(jdk1.5之后还可以通过设置访问标志中的ACC_SYNTHETIC标志位表示。)
StackMapTable属性
这是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
Signature属性
一个可选的定长属性,在JDK 1.5发布后增加的,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signature属性会为它记录泛型签名信息。这主要是因为Java的泛型采用的是擦除法实现的伪泛型,在字节码中泛型信息编译之后统统被擦除,在运行期无法将泛型类型与用户定义的普通类型同等对待。通过Signature属性,Java的反射API能够获取泛型类型。
BootstrapMethods属性
一个复杂的变长属性,位于类文件的属性表中,用于保存invokedynamic指令引用的引导方法限定符。(最多只能有一个)
Class类文件的结构