第 6 章:类文件结构

本文最后更新于:4 天前

Reference


在操作系统以上的应用层实现与平台无关
各种不同平台的 Java 虚拟机不与包括 Java 语言在内的任何程序语言绑定字节码所有平台都统一支持的程序存储格式,保证了任意一门功能性语言都可以表示为一个能被 Java 虚拟机所接受的有效的 Class 文件是构成平台无关性的基石,也是实现语言无关性的基础。

有一些 Java 语言本身无法有效支持的语言特性并不代表在字节码中也无法有效表达出来Java 语言的语义最终都会由多条字节码指令组合来表达,这决定了字节码指令所能提供的语言描述能力必须比 Java 语言本身更加强大才行

Class 类文件的结构:

  • 类或接口并不一定都得定义在文件里也可以动态生成,直接送入类加载器中,不需要以磁盘文件的形式存在。
  • Class 文件是一组以字节为基础单位当遇到需要占用单个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个字节进行存储的二进制流。因为没有任何分隔符号,所以各个数据项目的顺序、数量、数据存储的字节序都不允许改变。
  • Class 文件格式采用伪结构来存储数据,只有两种数据类型:
    • 无符号数:基本数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数。
      用于描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。
      • 集合:一个前置的容量计数器加若干个连续的数据项。
        描述同一类型但数量不定的多个数据。
    • 表:复合数据类型,由多个无符号数或者其他作为数据项构成。整个 Class 文件本质上也是一张表。
      用于描述有层次关系的复合结构的数据。
      • 集合:一个前置的容量计数器加若干个连续的数据项。
        描述同一类型但数量不定的多个数据。

通过实例看 Class 文件格式:

  1. 创建 TestClass.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    package io.weichao.clazz;

    public class TestClass {
    private int mm;

    public int incc() {
    return mm + 1;
    }
    }
  2. 使用 javac 编译出 TestClass.class
  3. 使用 javap 解析 Class 文件:
  4. 使用 WinHex 打开 Class 文件:
类型
名称
数量
描述实例中的值
u4
magic
1
魔数 0xCAFEBABE0xCAFEBABE
u2
minor_version
1
次版本号0x0000 = 0
u2
major_version
1
主版本号0x003F = 63
u2
constant_pool_count
1
常量池容量计数值0x0013 = 19
cp_info
constant_pool
constant_pool_count - 1
常量【1】0x0A = 10 ->CONSTANT_Methodref_info ->
    index 指向 CONSTANT_Class_info 的索引项: 0x0002 = 2 -> 【2】
    index 指向 CONSTANT_NameAndType_info 的索引项: 0x0003 = 3 -> 【3】
【2】0x07 = 7 ->CONSTANT_Class_info ->
    index 指向全限定名常量项的索引: 0x0004 = 4 -> 【4】
【3】0x0C = 12 ->CONSTANT_NameAndTpye_info ->
    index 指向该字段或方法名称常量项的索引: 0x0005 = 5 -> 【5】
    index 指向该字段或方法描述符常量项的索引: 0x0006 = 6 -> 【6】
【4】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x0010 = 16
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x6A6176612F60616E672F4F626A656374 = java/lang/Object
【5】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x0006 = 6
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x3C696E69743E = <init>
【6】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x0003 = 3
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x282956 = ()V
【7】0x09 = 9 ->CONSTANT_Fieldref_info ->
    index 指向声明字段的类或者接口描述符 CONSTANT_Class_info 的索引项: 0x0008 = 8 -> 【8】
    index 指向字节描述符 CONSTANT_NameAndType_info 的索引项: 0x0009 = 9 -> 【9】
【8】0x07 = 7 ->CONSTANT_Class_info ->
    index 指定全限定名常量项的索引: 0x000A = 10 -> 【10】
【9】0x0C = 12 ->CONSTANT_NameAndTpye_info ->
    index 指向该字段或方法名称常量项的索引: 0x000B = 11 -> 【11】
    index 指向该字段或方法描述符常量项的索引: 0x000C = 12 -> 【12】
【10】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x001A = 26
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x696F2F7765696368616F2F636C617A7A2F54657374436C617373 = io/weichao/clazz/TestClass
【11】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x0002 = 2
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x6D6D = mm
【12】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x0001 = 1
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x49 = I
【13】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x0004 = 4
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x436F6465 = Code
【14】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x000F = 15
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x4C696E654E756D6265725461626C65 = LineNumberTable
【15】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x0004 = 4
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x696E6363 = incc
【16】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x0003 = 3
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x282949 = ()I
【17】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x000A = 10
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x536F7572636546696C65 = SourceFile
【18】0x01 = 1 ->CONSTANT_Utf8_info ->
    length UTF-8 编码的字符串占用了字节数: 0x000E = 14
    bytes 长度为 length 的 UTF-8 编码的字符串: 0x54657374436C6173732E6A617661 = TestClass.java
u2
access_flags
1
访问标志0x0021 = 0x0001 | 0x0020 -> public
u2
this_class
1
类索引0x0008 = 8 -> 【8】
u2
super_class
1
父类索引0x0002 = 2 -> 【2】
u2
interfaces_count
1
接口计数器0x0000 = 0 -> 没有实现任何接口
u2
interfaces
interfaces_count
接口索引
u2
fields_count
1
字段表计数器0x0001 = 1
field_info
fields
fields_count
字段表access_flags: 0x0002 = 2 -> private
name_index: 0x000B = 11 -> 【11】
descriptor_index: 0x000C = 12 -> 【12】
attributes_count: 0x0000 = 0 -> 没有属性表
attributes:
u2
methods_count
1
方法表计数器0x0002 = 2
method_info
methods
methods_count
方法表access_flags: 0x0001 = 1 -> public
name_index: 0x0005 = 5 -> 【5】
descriptor_index: 0x0006 = 6 -> 【6】
attributes_count: 0x0001 = 1 -> 1 个属性表
    attribute_name_index: 0x000D = 13 -> 【13】
    attribute_length: 0x0000001D = 29
    max_stack: 0x0001
    max_locals: 0x0001
    code_length: 0x00000005
    code:
        0x2A -> 【aload_0】
        0xB7 = 【invokespecial】
        0x0001 = 1 -> 【1】
        0xB1 -> 【return】
    exception_table_length: 0x0000 = 0 -> 没有异常表
    exception_table:
    attributes_count: 0x0001 = 1 -> 1 个属性表
        attribute_name_index: 0x000E = 14 -> 【14】
        attribute_length: 0x00000006 = 6
        line_number_table_length: 0x0001 = 1
            start_pc: 0x0000 = 0
            line_number: 0x0003 = 3
access_flags: 0x0001 = 1 -> public
name_index: 0x000F = 15 -> 【15】
descriptor_index: 0x0010 = 16 -> 【16】
attributes_count: 0x0001 = 1 -> 1 个属性表
    attribute_name_index: 0x000D = 13 -> 【13】
    attribute_length: 0x0000001F = 31
    max_stack: 0x0002
    max_locals: 0x0001
    code_length: 0x00000007
    code:
        0x2A -> 【aload_0】
        0xB4 -> 【getfield】
        0x0007 = 7 -> 【7】
        0x04 = 4 -> 【iconst_<i>】
        0x60 = 【iadd】
        0xAC = 【ireturn】
    exception_table_length: 0x0000 = 0 -> 没有异常表
    exception_table:
    attributes_count: 0x0001 = 1 -> 1 个属性表
        attribute_name_index: 0x000E = 14 -> 【14】
        attribute_length: 0x00000006 = 6
        line_number_table_length: 0x0001 = 1
            start_pc: 0x0000 = 0
            line_number: 0x0007 = 7
u2
attributes_count
1
属性表计数器0x0001 = 1 -> 1 个属性表
attribute_info
attributes
attributes_count
属性表attribute_name_index: 0x0011 -> 【17】
attribute_length: 0x00000002
info: 0x0012 = 18 -> 【18】

魔数:

  • 唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件不使用扩展名来进行识别是因为扩展名可以随意改动
  • Class 文件的魔数是 0xCAFEBABE

版本号:
高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件《Java 虚拟机规范》在 Class 文件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件

常量池:

  • 是 Class 文件里的资源仓库,是 Class 文件结构中与其他项目关联最多的数据。
  • Class 文件结构中只有常量池的容量计数是从 1 开始当某些数据需要表达不引用任何一个常量池项目时,可以把索引值设置为 0
  • 主要存放两大类常量:
    • 字面量:如文本字符串、被声明为 final 的常量值等。
    • 符号引用:
      • 被模块导出或者开放的包。
      • 类和接口的全限定名。
      • 字段的名称和描述符。
      • 方法的名称和描述符。
      • 方法句柄和方法类型。
      • 动态调用点和动态常量。
  • 编译器会自动生成部分常量描述一些不方便使用固定字节进行表达的内容,供字段表方法表属性表引用。
  • 在 Class 文件中不会保存各个方法、字段最终在内存中的布局信息当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中
  • Java 程序中如果定义了超过 64 KB 英文字符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译Class 文件中方法、字段等都需要引用 CONSTANT_Utf8_info 型常量来描述名称,length 采用的 u2 类型能表达的最大值是 65535

访问标志:
用于识别一些类或者接口层次的访问信息。

三项数据确定某个类的继承关系:

  1. 类索引:确定类的全限定名。
  2. 父类索引只有一个,因为 Java 语言不允许多重继承:确定父类的全限定名默认是 java.lang.Object,java.lang.Object 的父类索引是 0
  3. 接口索引集合:描述类实现了哪些接口。

字段表集合:

  • 用于描述接口或者类中声明的变量Java 语言中的字段包括类级变量以及实例级变量,不包括在方法内部声明的局部变量
  • 不会列出从父类或者父接口中继承而来的字段但有可能会出现由编译器自动添加的字段,比如内部类中指向外部类实例的字段
  • 字段可以包括的修饰符:
    • 字段的作用域:public、private、protected。
    • 是实例变量还是类变量:static。
    • 可变性:final。
    • 并发可见性是否强制从主内存读写:volatile。
    • 可否被序列化:transient。
    • 字段数据类型:基本类型、对象、数组。
    • 字段名称。
  • 方法和字段的描述符是用来描述字段的数据类型、方法的参数列表包括数量、类型以及顺序和返回值。
    描述符规则:
    • [一个或多个前置的[:表示一维或多维数组。]
    • 一个大写字符:表示基本数据类型、无返回值的 void 类型。
    • L加对象的全限定名:表示对象类型。
  • 在 Java 语言中字段是无法重载的,但是对于 Class 文件格式来讲,只要两个字段的描述符不是完全相同的,那字段重名就是合法的。

方法表集合:

  • 对方法的描述。
  • 方法里的 Java 代码,经过 javac 编译成字节码指令之后,存放在方法表集合中 Code 属性里面。
  • 如果父类方法在子类中没有被重写 Override,方法表集合中就不会出现来自父类的方法信息但有可能会出现由编译器自动添加的方法,比如类构造器 <clinit>() 方法和实例构造器 <init>() 方法
  • 因为返回值不会包含在特征签名指一个方法中各个参数在常量池中的字段符号引用的集合中,所以 Java 语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的字节码的特征签名还包括方法返回值和受查异常表,所以可以合法共存于同一个 Class 文件中

属性表集合:

  • 存储一些额外信息。
  • Class 文件字段表方法表都可以携带自己的属性表集合。
  • 《Java 虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息Java 虚拟机运行时会忽略掉它不认识的属性

字节码指令:

  • Java 虚拟机的指令由操作码代表某种特定操作含义的数字,长度为一个字节和跟随其后的操作数零至多个代表此操作所需的参数构成。
    大多数指令都不包含操作数Java 虚拟机采用面向操作数栈而不是面向寄存器的架构,指令参数都存放在操作数栈中

  • 字节码指令集设计特点由 Java 语言设计之初主要面向网络、智能家电的技术背景所决定是追求尽可能小数据量、高传输效率:

    • 用一个字节来代表操作码。
      • 优:可以获得短小精干的编译代码。
      • 【引入问题】操作码总数不能够超过 256 条。
        【解决问题】Java 虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。大多数对于 boolean、byte、short 和 char 类型数据的操作,实际上都是使用相应的对 int 类型作为运算类型来进行
    • 放弃了操作数长度对齐。
      • 优:可以省略掉大量的填充和间隔符号。
      • 在处理超过一个字节的数据时,需要在运行时从字节中重建出具体数据的结构。
  • 最基本的执行模型不考虑异常处理

    1
    2
    3
    4
    5
    6
    7
    do {
    自动计算 PC 寄存器的值加 1;
    根据 PC 寄存器指示的位置,从字节码流中取出操作码;
    if ( 字节码存在操作数 )
    从字节码流中取出操作数;
    执行操作码所定义的操作;
    } whi1e ( 字节码流长度 > 0 );