Class文件

Class文件

起男 77 2022-12-06

Class文件

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。Class文件是一组以8位字节为单位的二进制流

格式

Class的结构不像XML等语言,由于没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,那个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变

Class文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照utf-8编码构成字符串值
  • 表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性的以_info结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明

结构

Class文件的结构并不是一成不变的,随着java虚拟机的不断发展,总是不可避免的会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的

Class文件的总体结构如下:

  • 魔数
  • Class文件版本
  • 常量池
  • 访问标志
  • 类索引,父类索引,接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

魔数

  • 每个Class文件开头的4个字节的无符号整数称为魔数(Magic Number)
  • 它的唯一作用是决定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即,魔数是Class文件的标识符
  • 魔数值固定为0xCAFEBABE。不会改变
  • 如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验时会直接抛出错误
  • 使用魔数而不是扩展名来识别主要是基于安全方面的考虑,因为文件扩展名可以随意的改动

版本号

  • 紧接着魔数的4个字节存储的是Class文件的版本号。同样也是4个字节。第5、6个字节所代表的就是编译的副版本号minor_version,而第7,8个字节就是编译的主版本号major_version
  • 它们共同构成了class文件的个数版本号。譬如某个Class文件的主版本号为M,副版本号为m,那么这个Class文件的格式版本号就确定为M.m
  • java的版本号是从45开始的,jdk1.1之后每个jdk大版本发布主版本号向上加1
  • 不同版本java编译期的Class文件对应的版本是不一样的。目前,高版本的java虚拟机可以执行由低版本编译器生成的Class文件,但低版本的虚拟机不能执行由高版本编译器生成的Class文件。否则jvm会抛出错误

常量池

  • 常量池是Class文件中内存最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用
  • 随着java虚拟机的不断发展,常量池的内容也日渐丰富。可以说,常量池是Class文件的基石
  • 在版本号之后,紧跟着的是常量池的数量,以及若干常量池表项
  • 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count)。与java语言习惯不一样的是,这个容量计数是从1开始,而不是从0开始
  • 常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放

常量池计数器

  • 由于常量池的数量不固定,所以需要放置两个字节来表示常量池容量计数值
  • 常量池容量计数值(u2类型):从1开始,表示常量池中由多少项常量。即constant_pool_count=1表示常量池中有0个常量项

常量池是从1开始的,把0空出来。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达式不引用任何一个常量池项的含义,这种情况用0表示

常量池表

  • constant_pool是一种表结构,以1到constant_pool_count-1为索引。表明了后面又多少常量项

  • 常量池主要存放两大类常量

    • 字面量(literal):文本字符串、声明为final的常量值

    • 符号引用(symbolic references):类和接口的全限定名、字段的简单名称和描述符、方法的简单名称和描述符

      全限定名:把全类名的.替换为/,为了使连续的多个不产生混淆,一般会在最后加上一个;表示结束

      简单名称:没有类型和参数修饰的方法或字段名称

      描述符:用来表述字段的数据类型,方法的参数列表和返回值

  • 它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征

    • 第一个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte(标记字节、标签字节)

虚拟机在加载class文件时才会进行动态连接,也就是说,class文件中不会保存各个方法和字段的最终内存布局,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译除了的直接引用一般会不同。如果又了直接引用,那说明引用的目标必定已经存在于内存中了

访问标识

  • 在常量池后,紧跟着访问标识。该标记使用两个字节来表示,用于识别一些类或者接口层次的访问信息
  • 类的访问权限通常为ACC_开头的常量
  • 每一种类型的表示都是通过设置访问标识的32位中的特定位来实现的。比如public final的类,标记为ACC_PUBLIC|ACC_FINAL
  • 如果带有ACC_INTERFACE标识则表示这是一个接口。同时也要设置ACC_ABSTRACT,并且不能设置成ACC_FINAL、ACC_SUPER和ACC_ENUM。注解类型必须设置成ACC_ANNOTATION,并且也要设置ACC_INTERFACE
  • ACC_SYNTHETIC表示该类或者接口是由编译器生成的,而不是由源代码生成的
  • ACC_ENUM表示该类型是枚举类型
  • ACC_SUPER用来向后兼容旧java编译器的来吗而设计的,jdk1.0.2之后编译出来的类默认都具有此标识

类索引,父类索引,接口索引集合

  • 在访问标记后,会指定该类的类别、父类类别和实现的接口
  • 格式:
    1. this_class:当前类,u2类型
    2. super_class:父类,u2类型。Object类没有父类
    3. interfaces_count:实现接口数量,u2类型
    4. interfaces[interfaces_count]:实现接口的数组,每个接口u2类型

字段表集合

字段计数器

  • 字段计数器(fields_count)的值表示当前class文件字段表集合的成员个数。使用两个字节来表示

字段表集合

  • 用于描述接口或类中声明的变量。字段包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量
  • 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述
  • 字段表集合中每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完整信息
  • 字段表集合不会列出父类或实现的接口中继承而来的字段,但有可能列出原本java代码中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段

在java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的

字段表

字段表作为一个表,同样有自己的结构:

  1. 访问标识,u2类型
  2. 字段名称索引,u2类型
  3. 描述符索引,u2类型
  4. 属性计数器,u2类型
  5. 属性集合,attribute_info类型。一些字段有可能还有一些属性信息,用于存储更多的额外信息。比如初始化值、注解信息等

方法表集合

方法计数器

  • 方法计数器(methods_count)的值表示当前class文件methods表的成员个数。使用两个字节表示

方法表集合

  • 在字节码文件中,每一个method_info项都对应着一个类或者接口中的方法信息。比如方法的修饰符、返回值类型以及参数类型
  • 如果这个方法不是抽象的或者native的,那么字节码中会体现出来
  • 一方面,methods表只描述当前类或接口中声明的方法,不包括从父类或父接口中继承的方法,另一方面,methods表有可能会出现由编译器自动添加的方法,最典型的便是编译器产生的方法信息,比如clinit和init方法

在Java语言中,重载一个方法,除了要有同样的方法名,还要拥有一个和原方法不同的方法参数,也就是返回值不会包含在特征签名之中,因此java语言里无法仅仅依靠返回值不同就对一个方法进行重载。但Class文件中,如果返回值不同,那么也是可以合法存在在一个class文件中的

方法表

方法表集合中的每一项都必须是一个method_info结构,具体结构如下

  1. 访问标志,u2类型
  2. 方法名索引,u2类型
  3. 描述符索引:u2类型
  4. 属性计数器:u2类型
  5. 属性集合:attribute_info类型

属性表集合

属性计数器

  • 属性计数器(attributes_count)的值表示当前class文件属性表的成员个数,使用两个字节表示

属性表集合

  • 方法表之后的属性表集合,指的是class文件所携带的辅助信息,比如该class文件的源文件的名称。以及任何带有RetentionPolicy.CLASS或者RetentionPolicy.RUNTIME的注解。这类信息常被用于java虚拟机的验证和运行,以及java程序的调试
  • 另外,字段表、方法表中都可以拥有自己的属性表。用于描述某些专有的信息
  • 属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但java虚拟机运行时会忽略掉它不认识的属性

属性表

属性表有很多种类型,不同的属性名对应不同的属性格式。属性表通用格式为:

  1. 属性名索引,u2类型
  2. 属性长度,u4类型
  3. 属性表,u1类型

当属性名为Code时:

  1. 属性名索引,u2类型
  2. 属性长度,u4类型
  3. 操作数栈深度的最大值,u2类型
  4. 局部变量表深度的最大值,u2类型
  5. 字节码指令长度,u4类型
  6. 字节码指令,u1类型
  7. 异常表长度,u2类型
  8. 异常表,exception_info类型
  9. 属性集合计数器,u2类型
  10. 属性集合,attribute_info类型,包含LineNumberTable、LocalVariableTable

附加属性SourceFile(字节码文件最后的属性表集合):

  1. 属性名索引,u2类型
  2. 属性长度,u4类型,值始终是2
  3. 源码文件索引(名称),u2类型