回顾:
java源码的运行会在编译期被编译成.class
字节码文件,然后再运行期由类加载器加载到内存,最终形成虚拟机直接使用的Java类
一、Class文件的结构
class文件是一组以8个字节为基础的二进制流,各个数据项目严格按照顺序紧凑排列在文件中,class文件中的存储内容几乎全部是程序运行的必要数据。
1)魔数与Class文件的版本
主要是检验class文件是否可以运行。
魔数: 出现在最开始的4个字节,作用是确定这个文件是否为一个被虚拟机接收的Class文件。类似一个身份识别。
class版本信息: 魔数后的4个字节,5-6是次版本号,7-8是主版本号。高版本的JDK可以兼容低版本的class文件,但低版本的JDK不能运行高版本的class文件。
2)常量池(※)
出现位置紧接着魔数和版本信息,它是Class文件的资源仓库,是Class文件结构中与其他项目关联最多的数据。
主要存放两大类常量——字面量和符号引用
-
字面量: 文本字符串,声明为final的常量值等(可见final类型其实在编译期就已经写入了类中)
-
符号引用: 在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替,等到类加载时会从常量池获得对应的符号引用,在类创建或运行时才会解析到具体的内存地址中。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这样的)。
因为常量池中常量的数值是不固定的,且有很多项目,如下图为常量池的项目类型,所以紧接着的字节码会定义常量池的容量(说明常量池的长度),常量池中的每一项常量都对应了一个表。通过常量的值查表可以的得知当前常量的类型与名称。
这十七种常量类型,又有各自完全独立的数据结构。比如CONSTANT_Class_info型。
name_index是常量池的索引值(第几个第几个)。他指向常量池中的CONSTANT_Utf8_info类型的常量,此常量代表了这个类的全限定名。
3)访问标志
常量池结束后,紧接着2个字节表示访问标志,这个标志用于识别一些类或接口层次的访问信息,比如:这个Class是类还是接口,是否是public的;是否是abstract;如果是类的话是否被声明为final等。它也对应了一张含义对照表。
4)类索引,父类索引接口索引集合
Class文件由这三项数据确定这个类的继承关系。类索引用于确定这个类的全限定类名,父类索引用于确定这个类父类的全限定类名(只有一个,因为java不支持多继承),接口索引集合,用于确定这个类是否实现了接口实现了哪些接口。
5)字段表集合(类级变量,实例级变量)
用于描述接口或类中声明的变量,比如变量的作用域,是否是static的,是否是final的,是否是volatile的,字段名称,数据类型等。修饰符用标志位来表示,字段的名字,被定义成什么类型的数据,只能引用常量池中的常量描述。(常量池中的CONSTANT_Utf8_Info型常量,同理方法也是)
6)方法表集合
与字段表集合相同,包括访问标志,名称索引,描述符索引属性表集合,其中描述符主要是用来描述字段的数据类型、或方法的参数列表和返回值。它也有自己的一套描述符规则。
7)属性表集合
Class文件,字段表,方法表,都可以携带自己的属性表集合,以描述某些场景的专有信息。
对于每一个属性,它的名称要从常量池中引用一个CONSTANT_Utf8_Info的常量,属性值的结构是完全自定义的,只需要给定长度去说明属性值所占的位数。
重要属性表——Code属性表
java程序方法体的代码经过javac后,会变成字节码指令存储在Code属性内,Code属性出现在方发表的属性集合中,但并非所有方法表都必须存在这个属性(比如接口,抽象类)。结构如下图ÿ