Class 文件结构

一. Class 类文件结构

  • Class 文件存储方式
    • 是一组以8字节为基础单位的二进制流
    • 各个数据项目严格按照顺序紧凑排列在 class 文件中
    • 中间没有任何分隔符, 这使得 class 文件中存储的内容几乎是全部程序运行的程序
  • Class 文件数据结构
    • 无符号数: 以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数
    • 表: 是由多个无符号数或者其他表作为数据项构成的复合数据类型, 所有的表都习惯以 info 结尾
  • Class 文件结构
    • Class 文件结构
  • 查看 class 反编译内容
    • javap -verbose Demo

1. 魔数与 Class 文件的版本(Magic Number/Version)

  • 魔数(Magic Number)
    • 每个 Class 文件的头 4 个字节称为魔数(Magic Number), 唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件
    • Class 文件 Magic Number 魔数为: 0xCAFEBABE(cafe babe)
  • 版本(Version)
    • minor_version(次版本号): 占2字节(前2字节用于表示次版本号), 0x0000
    • majro_version(主版本号): 占2字节(后2字节用于表示主版本号), 0x0031, 转化为十进制为49, 是使用JDK1.5编译的(JDK1.5:0x0031,JDK1.6:0x0032,JDK1.7:0x0033)
    • 这个的版本号是随着 jdk 版本的不同而表示不同的版本范围的。Java 的版本号是从 45 开始的。如果 Class 文件的版本号超过虚拟机版本, 将被拒绝执行

2. 常量池(Constant Pool)

  • 紧接着魔数与版本号之后的是常量池入口, 常量池简单理解为class文件的资源从库
    • 是 Class 文件结构中与其它项目关联最多的数据类型
    • 是占用 Class 文件空间最大的数据项目之一
    • 是在文件中第一个出现的表类型数据项目
  • 常量池之中主要存放两大类常量
    • 字面量: 比较接近于Java语言层面的常量概念,如文本字符串、被声明为 final 的常量值等
    • 符号引用: 属于编译原理方面的概念,包括了下面三类常量
      • 类和接口的全限定名
      • 字段的名称和描述符
      • 方法的名称和描述符
  • 由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)
    • constant_pool_count:占2字节,本例为0x0016,转化为十进制为22,即说明常量池中有21个常量(只有常量池的计数是从1开始的,其它集合类型均从0开始),索引值为1~22。第0项常量具有特殊意义,如果某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以将索引值置为0来表示
    • constant_pool:表类型数据集合,即常量池中每一项常量都是一个表,共有14种(JDK1.7前只有11种)结构各不相同的表结构数据。这14种表都有一个共同的特点,即均由一个u1类型的标志位开始,可以通过这个标志位来判断这个常量属于哪种常量类型,常量类型及其数据结构如下表所示

3. 访问标志(Access Flags)

  • 常量池之后的数据结构是访问标志(access_flags), 用于识别一些类或接口层次的访问信息,主要包括
    • 这个 Class 是类还是接口
    • 是否定义 public
    • 是否定义 abstract 类型
    • 如果是类的话是否被声明为 final 等

4. 类索引、父类索引、接口索引集合(This Class Name/Super Class Name/Interfaces)

  • Class 文件中由(this_class、super_class、interfaces_count 和 interfaces) 3 项数据来确定这个类的继承关系
    • this_class(类索引): 用于确定这个类的全限定名,占2字节
    • super_class(父类索引): 占 2 字节, 用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了 java.lang.Object 类之外所有类都有父类,故除 了java.lang.Object 类之外,所有类该字段值都不为 0)
    • interfaces_count 和 interfaces
      • interfaces_count(接口索引计数器): 占2字节。如果该类没有实现任何接口,则该计数器值为0,并且后面的接口的索引集合将不占用任何字节
      • interfaces(接口索引集合): 一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合中

5. 字段表集合(Fields)

  • Class 文件中字段表(field_info) 用来描述 interfaces 或者 Class 中申明的变量, 包括类级别变量、以及实例级别变量, 但是不包括在方法内部申明的变量
  • 字段表(field) 结构
    • access_flags: 字段访问标志 (字段修饰符放在中,占2字节,其值为0x0002)
    • name_index(名称索引) 和 descriptor_index(描述索引): 索引值, 对象常量池的引用, 分别是字段的简单名称和字段的和方法的描述符
    • attribute_info: 属性表集合
  • 在 Java 中描述一个字段的信息为以下, 这些修饰符都是布尔值。当在类和接口中定义的字段, 在字段表中,变量修饰符使用(字段访问标识)表示,字段数据类型和字段名称(由于不固定), 则引用常量池中常量表示
    • public、protected、private 修饰符
    • 类级别变量、实例级别变量(static修饰符)
    • 可变性(final修饰符)
    • 并发可见性(volatile修饰符)
    • 可序列化与否(transient修饰符)
    • 字段数据类型(基本类型、对象、数组)
    • 以及字段名称
  • fields_count:字段表计数器,即字段表集合中的字段表数据个数。占2字节,其值为0x0001,即只有一个字段表数据,也就是测试类中只包含一个变量(不算方法内部变量)
  • fields:字段表集合,一组字段表类型数据的集合。字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量

6. 方法表集合(Methods)

  • Class 文件中对方方法的描述与字段描述几乎采用完全一致的方式
  • 方法表(methods)结构
    • access_flags: 访问标志
    • name_index(名称索引) 和 descriptor_index(描述索引): 索引值, 对象常量池的引用, 分别是字段的简单名称和字段的和方法的描述符
    • attribute_info: 属性表集合
  • methods_count:方法表计数器,即方法表集合中的方法表数据个数。占2字节,其值为0x0002,即测试类中有2个方法
  • methods:方法表集合,一组方法表类型数据的集合。方法表结构和字段表结构一样:

7. 属性表集合(Attribute)

  • 属性表位置
    • 起始2位为0x0001,说明有一个类属性
    • 接下来2位为属性的名称,0x0010,指向常量池中第16个常量:SourceFile
    • 接下来4位为0x00000002,说明属性体长度为2字节
  • 属性表特性
    • Class 文件中其它数据项对长度、顺序、格式的严格要求不同,但是属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。
    • 虚拟机在运行时会忽略不能识别的属性,为了能正确解析 Class 文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性(预定义属性已经增加到21项):详细见 google 搜索属性表