Class文件概述

  • Class文件是一种二进制的文件类,它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码

  • JVM指令由一个字节长度的、代表某种特定含义的操作码(opcade)以及跟随其后的零个至多个代表此操作所需参数的操作数(operand)所构成。JVM中许多指令并不包含操作数,只有一个操作码

    image.png

  • 任何一个Class文件都对应着唯一一个类或接口的定义信息,并不一定以磁盘的形式存在,Class文件是一组以8位字节为基础的二进制流数

  • Class的结构并不像XML等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,字节的代表含义,长度,顺序都不允许改变

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

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

Class文件结构

  • Class文件结构并不是一成不变的,随着JVM的不断发展,总是不可避免地对Class文件结构做出一些调整,但是其基本结构和框架是十分稳定的

    image.png

类型名称说明长度数量
u4magic魔数4个字节1
u2minor_version副版本号2个字节1
u2major_version主版本号2个字节1
u2constant_pool_count常量池计数器2个字节1
cp_infoconstant_pool常量池表n个字节constant_pool_count-1
u2access_flags访问标时2个字节1
u2this_class类索引2个字节1
u2super_class父类索引2个字节1
u2interfaces_count接口计数器2个字节1
u2interfaces接口索引集合2个字节interfaces_count
u2fields_count字段计数器2个字节1
field_infofields字段表n个字节fields_count
u2methods_count方法计数器2个字节1
method_infomethods方法表n个字节methods_count
u2attributes_count属性计数器2个字节1
attribute_infoattributes属性表n个字节attributes_count

魔数(magic)

  • 每个Class文件开头4个字节的无符号整数称为魔数(Magic Number)

  • 魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的有效合法的Class文件,即魔数是Class文件的标识符

  • 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动

  • 魔数的值固定为为0xCAFEBABE,不会改变

  • 如果一个Class文件不以0xCAFEBABE开头,虚拟机在文件校验时会直接抛出ClassFormatError错误

    image.png

副版本号(minor_version)和主版本号(major_version)

  • 紧接着魔数4个字节存储的是Class文件的版本号,第5个和第6个字节所代表的含义是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version

  • 主版本号和副版本号共同构成了Class文件的格式版本号,即主版本号.副版本号

  • 版本号和Java编译器对应关系如下表:

    image.png

  • Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本的主版本号向上加1

  • 不同版本的Java编译器编译的Class文件对应的版本是不一样的,高版本的JVM可以执行由低版本编译器生成的Class文件(向下兼容),而低版本的JVM无法执行高版本编译器生成的Class文件,否则会报出UnsupportedClassVersionError错误

    image.png

常量池

  • Class文件使用一个前置的常量池计数器(constant_pool_count)加上若干个常量池表数据项(constant_pool)的形式来描述常量池的内容,我们把这一系列连续的常量池数据称为常量池集合
  • 常量池是Class文件中内容最为丰富的区域之一,常量池对于Class文件中的字段和方法解析也有着至关重要的作用
  • 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量的计数器(constant_pool_count),与Java语言的习惯不同,这个容量计数器索引是从1而不是0开始的
  • 常量池表数据项中,用于存放编译时期生成的字面量符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放(JDK1.7以后字符串常量池移至堆中)

常量池计数器(constant_pool_count)

  • 由于常量池表的数量不固定,所以需要防止两个字节来表示常量池容量计数器
  • 常量池容量计数值从1开始,标时常量池中有多少项常量,即constant_pool_count=1表示常量池中有0个常量项
  • 常量池把索引值0常量空出,这是为了满足后面某些指向常量池的索引值的数据在特定的情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示

常量池表(constant_pool)

  • 常量池表包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。

  • 常量池表中每一项都具备相同的特征,即第一个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte(标记字、标签字节)

  • 常量池主要存放两大类常量,字面量(Literal)符号引用(Symbolic Reference)

    • 字面量
      • 文本字符串
      • 声明为final的常量
    • 符号引用
      • 类和接口的全限定名
      • 字段的名称和描述符
      • 方法的名称和描述符
    • 描述符

    image.png

    image.png

访问标时(access_flags)

  • 访问标时使用两个字节来表示,用于识别一些类或接口的访问信息,包括这个Class是类还是接口,是否定义为public类型,是否定义为abstract类型,是否声明为final等,各种访问标记如下图所示

    image.png

    image.png

  • 类的访问标识通常为ACC_开头的常量

类索引、父类索引、接口计数器、接口索引集合

  • 这四项数据来确定这个类的继承关系

类索引(this_class)

  • 2字节无符号整数,指向常量池索引,提供了类的全限定名。this_class的值必须是对常量池表中某项的一个有效索引值

父类索引(super_class)

  • 2字节无符号整数,指向常量池索引,提供了当前类的父类的全限定名。如果我们没有继承任何类,其默认继承的是java/lang/Object类,同时由于Java不能多继承,所以其父类只有一个

接口计数器(interfaces_count)

  • interfaces_count项的值表示当前类或接口的实现或继承的接口数量

接口索引集合(interfaces[interfaces_count])

  • interfaces[interfaces_count]中每个成员的值必须是对常量池表中某项的一个有效索引值,个成员所表示的接口顺序和对应源代码中给定的接口顺序(从左到右)一样,即interfaces[0]对应的是源代码中最左边的接口

字段表集合

  • 用来描述接口或类中声明的变量,字段(field)不包括方法内部、代码块内部声明的局部变量
  • 字段名称,数据类型都是固定的,只能引用常量池中的常量来描述
  • 字段表描述了每个字段的完整信息,如字段的标识符、访问修饰符、是否为static、是否为final等
  • 字段表集合不会列出父类或者实现接口中继承来的字段,但有可能列出Java代码中不存在的字段,如在内部类中,为了保持对外部类的引用,会自动添加指向外部类实例的字段

字段表计数器(fields_count)

  • fields_count的值表示当前fields表的成员个数,使用两个字节表示

字段表(fields)

  • fields表中每个成员都是field_info结构的数据项,用于表示该类或接口某个字段的完整描述

  • 字段表的访问标识

    image.png

    image.png

  • 字段表结构

    image.png

方法表集合

  • 在字节码文件中,每一个method_info都对应着一个类或接口中的方法信息,比如方法的修饰符,返回值类型及方法参数信息等
  • 如果一个方法不是抽象或natice的,那么字节码中会体现出来
  • 方法表只描述当前类或接口中声明的方法,不包括从父类或接口中继承的方法,方法表中由可能出现由编译器自动添加的方法(比如类或接口的初始化方法<>(clinit)和实例化接口())

方法计数器(methods_count)

  • methods_count的值表示当前Class文件methods表的成员个数,使用两个字节表示

方法表(methods)

  • methods表中每个成员都是一个method_info结构,用于表示当前类或接口中某个方法的完整描述。如果某个method_info结构的access_flags项既没有设置ACC_NATIVE标志也没有ACC_ABSTRACT标志,那么该结构中也应该包含实现这个方法的虚拟机指令

  • method_info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法或接口初始化方法

  • 方法表结构

    image.png

  • 方法表的访问标识

    image.png

    image.png

属性表集合

  • 属性表集合指的是class文件所携带的辅助信息,比如该class文件的源文件名称,以及任何带有RetentionPolicy.CLASS或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试
  • 字段表,方法表有自己的属性表,用于描述某些场景专有的信息

属性计数器(attributes_count)

  • attributes_count的值表示当前Class文件attributes表的成员个数,使用两个字节表示

属性表(attributes)

  • 属性表通用结构

    image.png

  • 属性类型:属性表实际上可以有很多类型,Java8中定义了23种属性,下面是虚拟机中预定义的属性

    image.png

  • Code属性:

    • 该属性是结构表中的可变长度属性。属性包含 Java 虚拟机指令和方法的辅助信息,包括实例初始化方法或类或接口初始化方法

    • 如果该方法是本地方法或抽象方法,则其method_info结构的属性表中不得包含 Code 属性。否则,其method_info结构的属性表中必须只有一个 Code 属性

    • Code属性结构

      image.png

  • LineNumberTable属性

    • LineNumberTable属性是Code属性表中的可选可变长度属性,调试器可以使用它来确定代码数组的哪个部分对应于原始源文件中的给定行号

    • 如果代码属性的属性表中存在多个LineNumberTable属性,则它们可能以任意顺序出现

    • 在Code属性的属性表中,源文件的每行可能有多个LineNumberTable属性。也就是说,LineNumberTable属性可以一起表示源文件的给定行,而不必与源行一一对应

    • LineNumberTable属性结构

      image.png

  • LocalVariableTable属性

    • LocalVariableTable属性是代码属性的属性表中的可选可变长度属性,调试器可以使用它在方法执行期间确定给定局部变量的值

    • 如果代码属性的属性表中存在多个LocalVariableTable属性,则它们可能以任何顺序出现

    • 在代码属性的属性表中,每个局部变量只能有一个LocalVariableTable属性

    • LocalVariableTable 属性结构

      image.png

  • 更多属性及其结构含义可查看参考文档第一项的官方文档的 4.7. Attributes 文档介绍

参考文档