深入理解JVM(四)Class类的文件结构

1.Class文件的定义

Class文件时一组以8位字节为基础单位的二进制流,当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个*位字节进行存储。Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。

1.1.无符号数

无符号数属于基本的数据类型,根据这些值长度的不同分为:u1、u2、u4、u8,分别代表1字节的无符号数、2字节的无符号数、4字节的无符号数、8字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

1.2.表

表示由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯地以"_info结尾"。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由表6-1所示的数据项构成。

2.Class文件的构成

2.1.魔数

class文件的头4个字节称为魔数,它的唯一作用是确定这个文件能否为一个能被虚拟机接受的Class文件。

魔数的作用就相当于文件后缀名,只不过后缀名容易被修改,不安全,因此在class文件中标示文件类型比较合适。

class文件的魔数是用16进制表示的“CAFEBABE”,非常具有浪漫主义色彩,谁说程序员的情商都很低!

2.2.版本号

紧接着魔数的四个字节是class文件的此版本号和主版本号。 随着Java的发展, class文件的格式也会做相应的变动。 版本号标志着class文件在什么时候, 加入或改变了哪些特性。 举例来说, 不同版本的javac编译器编译的class文件, 版本号可能不同, 而不同版本的JVM能识别的class文件的版本号也可能不同, 一般情况下, 高版本的JVM能识别低版本的javac编译器编译的class文件, 而低版本的JVM不能识别高版本的javac编译器编译的class文件。 如果使用低版本的JVM执行高版本的class文件,JVM会抛出java.lang.UnsupportedClassVersionError 。

2.3.常量池

2.3.1. 什么是常量池?

紧接着版本号之后的就是常量池。常量池中存放两种类型的常量:字面值常量和符号引用。

字面值常量即我们在程序中定义的字符串、被final修饰的值。

符号引用就是我们定义的各种名字:类和接口的全限定名、字段的名字 和 描述符、方法的名字 和 描述符。

2.3.2 常量池的特点

常量池长度不固定

常量池的大小是不固定的,因此常量池开头放置一个u2类型的无符号数,用来存储当前常量池的容量。JVM根据这个值就知道常量池的头尾来。

注:这个值是从1开始的,若为5表示池中有4个常量。

常量池中的常量由而为表来表示

常量池开头有个常量池容量计数器,接下来就全是一个个常量了,只不过常量都是由一张张二维表构成,除了记录常量的值以外,还记录当前常量的相关信息。

常量池是class文件的资源仓库

常量池是与本class中其它部分关联最多的部分

常量池是class文件中空间占用最大的部分之一

2.3.3常量池中常量的特点

刚才介绍了,常量池中的常量大体上分为:字面值常量 和 符号引用。在此基础上,根据常量的数据类型不同,又可以被细分为14种常量类型。这14种常量类型都有各自的二维表示结构。每种常量类型的头1个字节都是tag,用于表示当前常量属于14种类型中的哪一个。

以CONSTANT_Class_info常量为例,它的二维表示结构如下:

CONSTANT_Class_info表:

类型名称数量

u1tag1

u2name_index1

tag表示当前常量的类型(当前常量为CONSTANT_Class_info,因此tag的值应为7,表示一个类或接口的全限定名);

name_index表示这个类或接口全限定名的位置。它的值表示指向常量池的第几个常量。它会指向一个CONSTANT_Utf8_info类型的常量,它的二维表结构如下:

CONSTANT_Utf8_info表:

类型名称数量

u1tag1

u2length1

u1byteslength

CONSTANT_Utf8_info表示字符串常量;

tag表示当前常量的类型,这里应该是1;

length表示这个字符串的长度;

bytes为这个字符串的内容(采用缩略的UTF8编码)

问:为什么Java中定义的类、变量名字必须小于64K?

类、接口、变量等名字都属于符号引用,它们都存储在常量池中。而不管哪种符号引用,它们的名字都由CONSTANT_Utf8_info类型的常量表示,这种类型的常量使用u2存储字符串的长度。由于2字节最多能表示65535个数,因此这些名字的最大长度最多只能是64K。

问:什么是UTF-8编码?什么是缩略UTF-8编码?

前者每个字符使用3个字节表示,而后者把128个ASKII码用1字节表示,某些字符用2字节表示,某些字符用3字节表示。

2.4.访问标志

在常量池之后是2字节的访问标志。访问标志是用来表示这个class文件是类还是接口、是否被public修饰、是否被abstract修饰、是否被final修饰等。

由于这些标志都由是/否表示,因此可以用0/1表示。

访问标志为2字节,可以表示16位标志,但JVM目前只定义了8种,未定义的标志位一律为0.

2.5.类索引、父类索引与接口索引集合

类索引(this_class) 和父类索引(super class) 都是一个u2类型的数据,而接口索引(interfaces) 是一组u2类型的数据的集合,Class 文件中由这三项数据来确定这个类的承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于Java 语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object 之外,所的Java 类都有父类,因此除了java lang.Object 外,所有Java 类的父类索引都不为0.接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句) 后的接口顺序从左到右排列在接口索集合中。

它们按照顺序依次排列,类索引和父类索引各自使用一个u2类型的无符号常量,这个常量指向CONSTANT_Class_info类型的常量,该常量的bytes字段记录了本类、父类的全限定名。

接口索引集合,入口的第一项-u2类型的数据为接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。

2.6.字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类及变量以及实例及变量,但不包括在方法内部声明的局部变量。

每一个字段表只表示一个成员变量,本类中所有的成员变量构成了字段表集合。

2.6.2 字段表结构的定义

类型                             名称                        数量

u2                         access_flags                      1

u2                         name_index                        1

u2                         descriptor_index                  1

u2                         attributes_count                   1

attribute_info         attributes              attributes_count

access_flags

字段的访问标志。在Java中,每个成员变量都有一系列的修饰符,和上述class文件的访问标志的作用一样,只不过成员变量的访问标志与类的访问标志稍有区别。

name_index

本字段名字的索引。指向一个CONSTANT_Class_info类型的常量,这里面存储了本字段的名字等信息。

descriptor_index

描述符。用于描述本字段在Java中的数据类型等信息(下面详细介绍)

attributes_count

属性表集合的长度。

attributes

属性表集合。到descriptor_index为止是字段表的固定信息,光有上述信息可能无法完整地描述一个字段,因此用属性表集合来存放额外的信息,比如一个字段的值。(下面会详细介绍)

2.6.3 什么是描述符

成员变量(包括静态成员变量和实例变量) 和 方法都有各自的描述符。

对于字段而言,描述符用于描述字段的数据类型;

对于方法而言,描述符用于描述字段的数据类型、参数列表、返回值。

在描述符中,基本数据类型用大写字母表示,对象类型用“L对象类型的全限定名”表示,数组用“[数组类型的全限定名”表示。

描述方法时,将参数根据上述规则放在()中,()右侧按照上述方法放置返回值。而且,参数之间无需任何符号。

2.6.4 字段表需要注意的点

一个class文件的字段表集合中不能出现从父类/接口继承而来字段;

一个class文件的字段表集合中可能会出现程序猿没有定义的字段

如编译器会自动地在内部类的class文件的字段表集合中添加外部类对象的成员变量,供内部类访问外部类。

Java中只要两个字段名字相同就无法通过编译。但在JVM规范中,允许两个字段的名字相同但描述符不同的情况,并且认为它们是两个不同的字段。

Class文件的构成

2.7:方法表的集合

在class文件中,所有的方法以二维表的形式存储,每张表来表示一个函数,一个类中的所有方法构成方法表的集合。

方法表的结构和字段表的结构一致,只不过访问标志和属性表集合的可选项有所不同。

类型名称数量

u2access_flags1

u2name_index1

u2descriptor_index1

u2attributes_count1

attribute_infoattributesattributes_count

方法表的属性表集合中有一张Code属性表,用于存储当前方法经编译器编译过后的字节码指令。

方法表集合的注意点

如果本class没有重写父类的方法,那么本class文件的方法表集合中是不会出现父类/父接口的方法表;

本class的方法表集合可能出现程序猿没有定义的方法

编译器在编译时会在class文件的方法表集合中加入类构造器和实例构造器。

重载一个方法需要有相同的简单名称和不同的特征签名。JVM的特征签名和Java的特征签名有所不同:

Java特征签名:方法参数在常量池中的字段符号引用的集合

JVM特征签名:方法参数+返回值

2.8:属性表的集合

在Class文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与Class文件中其它的数据项目要求的顺序、长 度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

你可能感兴趣的:(深入理解JVM(四)Class类的文件结构)