深入理解Java虚拟机(八)——Class类文件结构

352 阅读7分钟

无关性

Java的平台无关性

Java有一句响亮的口号:“一次编写,到处运行”,也就是说Java程序编写一次,就可以运行在任意的硬件系统上。实现这一承诺,离不开Java虚拟机的存在:Java为每种硬件系统编写了不同的虚拟机,帮助运行在虚拟机之上的Java程序屏蔽掉了硬件系统的底层实现,即不同的硬件系统为Java程序的运行提供了环境,因此实现了Java程序可以运行在任意的硬件系统上。

JVM的语言无关性

我们知道,Java程序需要Java虚拟机才能实现运行,然而,Java虚拟机并不只支持Java程序的运行,其他的程序语言,例如JRuby、Jython等也可在Java虚拟机上运行。

无关性的基石

实现上述无关性的基础是以字节码方式存储数据的类文件结构。

Class文件结构概述

●Class文件是二进制文件,它的内容有严格的规范,各个数据项目严格按照规范排列在Class文件里,中间没有空隙存在。

●Class文件中只有两种数据类型:无符号数和表。

  • ●无符号数属于基本的数据类型,有u1、u2、u4、u8之分,分别代表1个字节的无符号数、2个字节的无符号数、4个字节的无符号数、8个字节的无符号数。
  • ●表是由多个无符号数或其他表作为数据项构成的复合数据结构,所有表都习惯性按“_info”结尾。

●Class类文件的结构组成(严格按照顺序):魔数、版本号、常量池、访问标志、类索引、父类索引、接口索引集合、字段表集合、方法表集合、属性表集合。

Class文件组成1:魔数

每个Class文件的头四个字节被称为魔数,作用是确定当前文件是否为Class文件。

Class文件组成2:版本号

魔术过后是版本号,作用是记录当前Class文件的版本,也就是JDK的版本。需要注意的是,高版本的JDK能向下兼容低版本的Class文件,但不能运行更高版本Class文件,即便文件结构没有发生改变。

Class文件组成3:常量池

常量池的特点

1.版本号过后是常量池入口,常量池可以理解为Class文件中的资源库,是Class文件与其他项目关联最多的数据类型,也是Class文件中空间最大的数据项目之一。

2.常量池中常量的数量不固定,因此在常量池的入口有一个u2类型数据,用作常量池容量计数器。

3.常量池容量计数器的索引是从1而不是0开始,也就是说当计数器值为6时,表示有5个常量。Class文件中只有常量池的容量计数是从1开始的。

4.常量池主要存放两大类常量:字面量和符号引用。

  • ●字面量:主要是文本字符串、final修饰的常量等。
  • ●符号引用:包括三类常量,分别是类和接口的全限定符、字段的名称和描述符、方法的名称和描述符。

常量池的作用

Java代码在编译成字节码的时候,并不会在Class文件里保存各个方法、字段的最终内存布局信息,而在等类加载器加载Class文件的时候才进行动态的连接,因此这些方法、字段在只有在运行期得到转换才能获取真正的内存入口地址,才能真正被使用。而常量池的作用就是存储虚拟机提供所需的符号引用,这些符号引用在虚拟机运行时被解析到具体的内存地址中。

常量池中的常量

常量池中的常量都是表结构,总共有14种,他们的共同特点是表开始的第一位是u1类型的标志位,表示当前这个常量属于哪种常量类型。

下面以常量CONSTANT_Class_info为例解析常量的结构:

类型 名称 数量
u1 tag 1
u2 name_index 1

●tag为标志位,表示当前常量属于14种常量中的哪一种。若该标志位0x07,则说明该常量是CONSTANT_Class_info类型,代表一个类或接口的符号引用。

●name_index是一个索引值,它指示虚拟机按照这个索引值寻找常量池中的某一个常量,而这个常量是CONSTANT_Utf8_info类型,表示UTF-8编码的字符串类型。在这里该常量存储的是类或接口的全限定名。

常量CONSTANT_Utf8_info的类型如下:

类型 名称 数量
u1 tag 1
u2 length 1
u1 bytes length

tag为标志位,若该标志值为0x01,说明该常量是CONSTANT_Utf8_info类型,代表一个长度为length的字符串bytes。

超过64KB大小的变量或方法名不能被编译

Class文件中的字段和方法表都需要引用CONSTANT_Utf8_info来描述名称,而CONSTANT_Utf8_info中的length为u2类型,最大值为65535,表示描述名称的字符串最大为65535字节长,即约为64KB。

Class文件组成4:访问标志

常量池过后是访问标志,该标志用于识别一些类或接口层次的访问信息。

(注:图片来源于网络,若有侵权请联系删除)

Class文件组成5:类索引、父类索引、接口索引集合。

●类索引和父类索引都是一个u2类型的数据,用于确定类和父类的全限定名。类索引、父类索引这两个值各自指向在常量池中的CONSTANT_Class_info类型常量,通过这个常量中的索引值又可以找到一个CONSTANT_Utf8_info类型的常量,也就是找到存储在CONSTANT_Utf8_info常量里的全限定名字符串。

●接口索引集合是一组u2类型的数据集合,用来描述多个接口的全限定名。

Class文件组成6:字段表集合

字段表的作用

字段表用于描述类或接口中声明的变量。字段包括类级变量(static)、实例级变量,但不包括方法体里声明的局部变量。

字段表结构

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

●access_flags:字段访问标志,通过设置该值来描述字段的信息,例如是否为public、是否为private等。

●name_index:简单名称索引,是对常量池中常量的引用,描述字段的简单名称。

●descriptor_index:字段的描述符,是对常量池中常量的引用。

●attributes_count:属性表集合计数器。

●attributes:属性表,用于描述字段的一些额外的信息,例如字段的值等。

关于描述符的理解

字段表和方法表里都有描述符的存在,他们的作用如下:

  • ●对于字段表来说,描述符描述字段的数据类型。

  • ●对于方法表来说,描述符描述参数列表、返回类型。

字段表的特点

●字段表不会存储从父类或者父接口继承而来的字段。

●字段表会自行添加一些必要的字段,例如为了支持内部类对外部类的访问,会在内部类中的字段表中添加一个外部类的实例字段。

●Java语言不支持同名字段的存在,而在Class文件里,名称相同而描述符不同的两个字段是可以同时存在的。

Class文件组成7:方法表集合

方法表描述是方法的信息,它的表结构和字段表结构是一致的,参考上文的字段表结构即可。

方法表的特点

●如果父类方法没有在子类中重写,那么方法表集合中就不会有来自父类的方法信息。

●方法表中存在编译器自行添加的方法,例如构造方法。

●Java语言重载的方法需要简单名称相同,特征签名不同(方法参数属于特征签名,方法返回值不属于);而Class文件只要求方法的描述符不相同即可,也就是简单名称和特征签名都相同的方法,只要返回值不同,就可以共存在Class里。

Class文件组成8:属性表集合

在Class文件、字段表、方法表都可以携带自己的属性表集合,用来描述一些额外的信息。其中Code属性是最重要的一个属性:方法里的代码经过编译后变成字节码指令,存放在方法表的Code属性里。