类文件结构概述
记得在第一节计算机程序课上老师就讲过:“计算机只认识0和1,所以我们写的程序需要经编译器翻译成由0和1构成的二进制格式才能由计算机执行”。至今为止,我们的计算机仍然只能识别0和1.java语言具有平台无关性,因为有jvm的支持,编译过后的java代码可以在任意平台上运行。java语言经编译后变成class文件,class文件在jvm上运行。现在jvm添加对其他语言的支持,其他语言编译成class文件也可以在jvm上运行,所以,jvm不仅提供了平台无关性,还提供了语言无关性。在jvm规范中定义了class文件格式,所有能够在jvm上运行的class文件必须要满足jvm定义的class文件规范,不是随随便便改个后缀名就能在jvm上运行,那jvm就危险了。
class 类文件的结构
- Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在class文件中,中间没有任何分隔符,这使得class文件中存储的内容几乎是全部程序运行的程序。Java虚拟机规范规定,Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。
-
无符号数属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节。
-
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。那么表是干嘛的呢?表主要用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:
-
字节码文件解析:
将下面一点代码使用JDK1.8编译成Class文件进行讲解
package org.fenixsoft.clazz;
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
通过命令javac将这个类编译成classs文件。notepad++装一个HEX-Editor 插件后,打开这个class文件。
魔数
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为 一个能被虚拟机接受的Class文件。
版本号
紧接着魔数的4个字节存储的是class文件的版本号,第5和第6个字节是次版本号,第7和第8个字节是主版本号。这里大概说一下java版本号的规则,java的版本号是从45开始的,JDK之后的每个JDK大版本发布主版本号向上加1(它之前的JDK1.0-1.1使用的是45.0-45.3),高版本的JDK能向下兼容以前版本的class文件,但不能运行以后版本的class文件,即使class文件格式没有发生任何变化,虚拟机也必须拒绝执行超过其版本号的class文件。我的是1.8
常量池
-
紧接着主次版本号之后的是常量池入口,常量池可以理解为class文件之中的资源仓库,它是class文件结构中于其他项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,同时它还是在class文件中第一个出现的表类型数据项目。由于常量的数量不确定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。值得一提的是,和java习惯不一样的是,这个容量计数是从1开始的,而不是从0开始了。看我打开的这个class文件的常量池容量是0x003A,即十进制的58,这就代表常量池中有58项常量,索引值为1-58
。 -
符号引用与直接引用的关联
- 符号引用是一组符号,用来描述所引用的目标,符号是以任何形式存在的字面量。对于符号引用Java虚拟机并没有严格的限制。规定只需要使用的时候能够无歧义定位到目标就可以。常量池存在于Class文件中,而Class文件是必须首先通过Java虚拟机的类加载机制加载到内存中(确切的说是方法区这个内存区域,回顾一下,方法区存放的主要是对象的实例,这个Class文件是虚拟机对外接受访问的接口)。符号引用属于常量池中的内容,那么是不是说符号引用的目标已经加载到内存中了呢?答案是否定的,因为符号引用与虚拟机的内存布局无关,符号引用的目标并不一定已经加载到内存中了。
- 直接引用可以是直接指向引用目标的指针、相对偏移量或者是一个能够间接定位到目标的句柄。直接引用是和虚拟机的内存布局有关的,同一个符号引用在不同的虚拟机上翻译的直接引用一般是不同的。如果有了直接引用,那么引用的目标必定是存在内存中的。
-
在常量池中每一项常量都是一个表,在jdk1.7中共有14中常量类型,所以常量池的项目就对应14张表,这14张表的每种类型都不一样。但是有一个共同特点:表开始的第一位都是一个u1类型的标志位,代表这个常量属于哪种类型。
- 我们可以对照表查看常量对应的类型,下图我的里面对应的是0x0A,十进制是10,在表中就是指向声明方法的类描述符。这个字节是标志位,标志的是什么类型,我们已经知道了类型是10,然后按照表,接下来的四个字节(分为两个2字节)分别代表的意思看图表。
访问标记
这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等
类索引、父类索引和接口索引集合
这个数据项主要用于确定这个类的继承关系。
其中类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据。在Java中由于不允许多继承,所以父类索引是唯一的,但是一个类可以实现多个接口,所以得到的接口索引是一个集合,表示这个类实现了哪些接口。
字段表集合
存储类中的成员变量,包括实例变量和类变量,其结构如下:
-
access_flags:字段的访问标志,字段的类型,是否为public,是否为final,是否为volatile,是否为transient等。
-
name_index:字段名索引,最终指向constant_utf8_info中的字符串,代表字段的简单名称。
-
descriptor_index:字段描述符索引,用于描述字段的数据类型。基本数据类型的描述符用大写字母表示,如果字段是对象类型,用大写L表示,如果字段是数组类型,用[表示这是个数组。
-
attributes_count:属性表长度
-
attributes:存放字段的额外信息,比如说,如果定义为final类型的字段,初始化时由初始值,初始值存放在CONSTANT_VALUE属性中。
方法表集合
Class文件存储 格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依 次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表 集合(attributes)几项。
因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了 ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对,synchronized、native、strictfp和abstract 关键字可以修饰方法,方法表的访问标志中也相应地增加了ACC_SYNCHRONIZED、 ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。对于方法表,所有标志位及其取值可参见下表:
属性表集合
前面的Class文件、字段表和方法表都可以携带自己的属性信息,这个信息用属性表进行描述,用于描述某些场景专有的信息。在属性表中没有类似Class文件的数据项目类型和顺序的严格要求,只要新的属性不与现有的属性名重复,任何人都可以向属性表中写入自己定义的属性信息。
- Code属性
Java程序方法体中的代码经过javac编译最终编译成的字节码指令就保存在Code属性中。但是并非所有的方法表都必须存在这个属性。Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code)和元数据(Metadata,包括类、字段、方法定义及其其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有其他的数据项目都用于描述元数据。
- Exceptions属性
这个属性的作用是列举出方法中可能抛出的受查异常(Checked Exception),也就是描述throws 后的列举的异常
- LineNumberTable属性
主要用于描述Java源代码行号与字节码行号之间的对应关系。这个属性也不是必须的。如果没有这个属性,对程序的直接影响就是当抛出异常的时候无法显示对应的行号;并且在调试的时候无法通过设置断点的方法是调试程序。
- LocalVariableTable属性
用于描述栈帧中局部变量表中的变量与Java源码中定义的变量的之间的关系。也不属于必须的属性。如果没有这个属性,产生的直接影响就是当别人引用这个方法的时候,所有的参数名称都会丢失,IDE将会使用诸如args0、args1之类的参数进行显示。自然,当调试程序的时候,显示的参数名称是不可知的。
- SourceFile属性
用于记录这个Class文件的源码文件名称。如果不使用这个属性,那么当抛出异常的时候,堆栈中将不会显示出错代码所属的文件名。
- ConstantValue属性
作用是通知虚拟机自动为静态变量赋值。要注意的是,只有被static关键字修饰的额变量才可以使用这个属性(类变量)。对于非类变量,初始化是在方法中进行的;对于类变量可以选择两种方式进行变量的初始化:一是在类构造器方法中使用;二是是ConstantValue属性。目前Sun Hotspot的选择原则是:如果一个变量同时使用static和final关键字修饰,并且这个变量是基本数据类型或者java.lang.String类型的话,就使用ConstantValue属性进行初始化。如果没有被final修饰或者并非是基本数据类型,那么将会选择使用方法进行初始化。
- InnerClass属性
这个属性主要用于记录内部类与宿主类之间的关联关系。
- Deprecated以及Synthetic属性
这两个属性都属于标志类型的布尔属性,只存在有没有的区别。
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,可以通过注解@deprecated实现
Synthetic属性代表此字段并不是由Java源码产生的,而是通过编译器自行添加的。
StackMapTable属性
该属性的目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
- Signature属性
这个属性是专门用来记录泛型类型的,因为在Java语言采用的是擦除法实现的泛型,在字节码(Code属性)中,泛型信息编译之后会被擦除。擦除法的优点是能够节省泛型所占的内存空间,缺点是在运行期间无法通过反射得到泛型信息,而Signature属性则弥补了这一缺陷。现在的Java反射API已经能够得到泛型信息,功劳就在于这个属性。
- BootstrapMethods属性
这个属性用于保存invokedynamic指令引用的引导方法限定符。该指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
方法表集合
方法表的命名和字段表差不多