认识 .class 文件的字节码结构

由于最近在学习 ASM 的使用,在学习 ASM 使用之前首先需要了解 .class 文件字节码的结构以及 JVM 虚拟机执行引擎等知识,本篇文章通过一个简单的例子认识 .class 文件的字节码结构。



一. 简介

写一个简单的 Demo.java 程序如下所示

1
2
3
4
5
6
7
8
9
10
package com.lijiankun24.classpractice;
public class Demo {
private int m;
public int inc() {
return m + 1;
}
}

使用 javac 命令编译 Demo.java 文件生成 Demo.class 文件

1
$ javac Demo.java

接着我们用纯文本编辑器打开生成的 Demo.class 文件,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 4465 6d6f 2e6a 6176 610c
0007 0008 0c00 0500 0601 0004 4465 6d6f
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7400 2100 0300 0400 0000 0100 0200
0500 0600 0000 0200 0100 0700 0800 0100
0900 0000 1d00 0100 0100 0000 052a b700
01b1 0000 0001 000a 0000 0006 0001 0000
0001 0001 000b 000c 0001 0009 0000 001f
0002 0001 0000 0007 2ab4 0002 0460 ac00
0000 0100 0a00 0000 0600 0100 0000 0600
0100 0d00 0000 0200 0e

可以看到,该文件中是由十六进制符号组成的,这一段十六进制符号组成的长串是遵守 Java 虚拟机规范的

二. Java 虚拟机规范

在 Java 虚拟机规范中规定了 Java 虚拟机结构、Class 类文件结构、字节码指令等内容,可以参考 GitHub 上的《Java 虚拟机规范》

Java 虚拟机的类文件是一组以 8 位字节为基础单位的二进制流,各数据项目严格按照顺序紧凑地排列在 .class 文件中,中间没有添加任何分隔符,这使得整个 .class 文件中存储的内容几乎全都是程序需要的数据,没有空隙存在

2.1 Java 虚拟机

  1. Java 虚拟机就是一个虚拟的计算机,与真实的计算机一样,Java 虚拟机有自己完善的硬件体系,如处理器、堆栈、寄存器,还有相应的指令集系统。虚拟机与真实电脑的唯一区别就是:虚拟机的处理器、内存堆栈是用软件虚拟出来的,而真实的电脑的处理器、内存则是真真实实存在的
  2. 虽然名字是叫 Java 虚拟机,但是 Java 虚拟机与 Java 语言并没有直接的关系,它只是按照 Java 虚拟机规范去读取 .class 文件,并按照规定去解析、执行字节码指令
  3. 准确地说,Java 虚拟机与字节码文件(.class 文件)绑定,如果足够牛逼,可以写一个编译器将 C 语言代码编译成符合 Java 虚拟机规范的字节码文件,那么 Java 虚拟机也是可以执行的

2.2 class 类文件结构

  1. 在 Java 虚拟机规范中定义了 .class 文件字节码的结构和规范,.class 字节码文件可以用两种数据类型来表示:无符号数和表
  2. 无符号数属于最基本的数据类型,以 u1、u2、u4、u8 分别代码 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值
  3. 表是由无符号数或其他表作为数据项构成的复合数据结构,所有表都习惯性地以 “info” 结尾,表用于描述有层次关系的复合结构的数据
  4. 整个 .class 文件本质上就是一张表,由下表所示的数据项构成



  5. 上面的表其实可以划分为以下七个部分,.class 字节码文件包括:

    • 魔数与class文件版本
    • 常量池
    • 访问标志
    • 类索引、父类索引、接口索引
    • 字段表集合
    • 方法表集合
    • 属性表集合
  6. 无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项组成,称这一系列连续的某一类型的数据为某一类型的集合

三. class 文件详解

我们就按照上述讲的 7 个部分详细介绍 Demo.class 字节码文件的结构和内容

3.1 魔数和 class 文件版本

在魔数和 class 文件版本中有如下四点需要介绍:

  1. 魔数(Magic Number):.class 文件的第 1 - 4 个字节,它唯一的作用就是确定这个文件是否是一个能被虚拟机接受的 class 文件,其固定值是:0xCAFEBABE(咖啡宝贝)。如果一个 class 文件的魔术不是 0xCAFEBABE,那么虚拟机将拒绝运行这个文件
  2. 次版本号(minor version):.class 文件的第 5 - 6 个字节,即编译该 .class 文件的 JDK 次版本号
  3. 主版本号(major version):.class 文件的第 7 - 8个字节,即编译该 .class 文件的 JDK 主版本号
  4. Note:高版本的 JDK 能向下兼容低版本的 .class 文件,但不能运行新版本的 .class 文件。例如一个 .class 文件是使用 JDK 1.5 编译的,那么我们可以用 JDK 1.7 虚拟机运行它,但不能用 JDK 1.4 虚拟机运行它。各个版本的 SDK 的次版本号和主版本号如下表所示


实例:在上面编译 Demo.java 生成的 Demo.class 文件中,Magic Number:cafe babe,minor version:0000,major version:0034,可见我们是使用 JDK 1.8 编译生成的 Demo.class 文件

3.2 常量池

紧接着主次版本号之后的是常量池的入口,常量池可以理解为 class 文件之中的资源仓库,它是 class 文件结构中与其他项目关联最多的数据类型,也是占用 class 文件空间最大的数据项之一

常量池由两部分组成:常量池常量计数器和常量池

  • 常量池常量计数器(constant_pool_count):.class 文件中第 9 -10 个字节
  • 常量池(constant_pool):是紧跟在常量池常量计数器后面的内容,在常量池中的每个常量都用一个类型为 cp_info 的表表示,该表有 14 个值,分别是


实例:在上面编译 Demo.java 生成的 Demo.class 文件中,constant_pool_count:0013,表示有 19 个常量;常量池中的内容分别如下:

  1. 第一个常量
    • 紧接着 0013 后面的,是 0a,从上表中查得此常量是一个 CONSTANT_Methodref_info 常量
    • 该常量项第 2 - 3 个字节表示类信息,这里是 00 04 表示指向常量池中第 4 个常量所表示的信息
    • 该常量项第 4 - 5 个字节表示名称及类描述符,这里值为 00 0f 表示指向常量池第 16 个常量所表示的信息
  2. 第二个常量
    • 紧接着 00 0f 之后的一个字节是 09,从上表中查得此常量是一个 CONSTANT_Fieldref_info 常量
    • 该常量项第 2 - 3 个字节表示类信息,这里是 00 03,表示指向常量池第 13个常量所表示的信息
    • 该常量项第 4 - 5 个字节表示名称及类描述符,这里值为 00 10 表示指向常量池第 16 个常量所表示的信息
  3. 第三个常量
    • 紧接着 00 10 之后的一个字节是 07,从上表中查得此常量是一个 CONSTANT_Class_info 常量
    • 该常量项第 2 - 3 个字节表示字符串字面量的索引,这里值是 00 11 表示指向常量池的第 17 个常量
  4. 第四个常量
    • 紧接着 00 11 之后的一个字节是 07,表示该常量项是一个 CONSTANT_Class_info 常量项
    • 该常量项第 2 - 3 个字节表示字符串字面量的索引,这里值是 00 12 表示指向常量池的第 18 个常量
  5. 第五个常量
    • 紧接着 00 12 之后的一个字节是 01,表示该常量项是一个 CONSTANT_Utf8_info 字符串常量
    • 该常量项第 2 - 3 个字节表示该字符串的长度,这里是 00 01 表示该字符串长度为 1 个字节
    • 紧接着 00 01 之后的 1 个字节是 6d,是一个字符串常量,转换之后是:m
  6. 第六个常量
    • 紧接着 6d 之后的一个字节是 01,表示该常量项是一个 CONSTANT_Utf8_info 字符串常量
    • 该常量项第 2 - 3 个字节表示该字符串的长度,这里是 00 01 表示该字符串长度为 1 个字节
    • 紧接着 00 01 之后的 1 个字节是 49,是一个字符串常量,转换之后是:I
  7. 第七个常量
    • 紧接着 6d 之后的一个字节是 01,表示该常量项是一个 CONSTANT_Utf8_info 字符串常量
    • 该常量项第 2 - 3 个字节表示该字符串的长度,这里是 00 06 表示该字符串长度为 6 个字节
    • 紧接着 00 06 之后的 6 个字节是 3c 696e 6974 3e,是一个字符串常量,转换之后是:
  8. 第八个常量
    • 紧接着 3e 之后的一个字节是 01,表示该常量项是一个 CONSTANT_Utf8_info 字符串常量
    • 该常量项第 2 - 3 个字节表示该字符串的长度,这里是 00 03 表示该字符串长度为 3 个字节
    • 紧接着 00 03 之后的 3 个字节是 2829 56,是一个字符串常量,转换之后是:()V
  9. 第九个常量
    • 紧接着 56 之后的一个字节是 01,表示该常量项是一个 CONSTANT_Utf8_info 字符串常量
    • 该常量项第 2 - 3 个字节表示该字符串的长度,这里是 00 04 表示该字符串长度为 4 个字节
    • 紧接着 00 04 之后的 4 个字节是 436f 6465,是一个字符串常量,转换之后是:Code
  10. 第十个常量
    • 紧接着 65 之后的一个字节是 01,表示该常量项是一个 CONSTANT_Utf8_info 字符串常量
    • 该常量项第 2 - 3 个字节表示该字符串的长度,这里是 00 0f 表示该字符串长度为 15 个字节
    • 紧接着 00 0f 之后的 15 个字节是 4c 696e 654e 756d 6265 7254 6162 6c65,是一个字符串常量,转换之后是:LineNumberTable
  11. 第十一个常量
    • 紧接着 65 之后的一个字节是 01,表示该常量项是一个 CONSTANT_Utf8_info 字符串常量
    • 该常量项第 2 - 3 个字节表示该字符串的长度,这里是 00 03 表示该字符串长度为 3 个字节
    • 紧接着 00 03 之后的 3 个字节是 69 6e63,是一个字符串常量,转换之后是:inc
  12. 第十二个常量:0100 0328 2949,是一个字符串常量,转换之后是:()I
  13. 第十三个常量:0100 0a53 6f75 7263 6546 696c 65,是一个字符串常量,转换之后是:SourceFile
  14. 第十四个常量:01 0009 4465 6d6f 2e6a 6176 61,是一个字符串常量,转换之后是:Demo.java
  15. 第十五个常量
    • 0c 0007 0008,是一个 CONSTANT_NameAndType_info 常量项
    • 该常量项第 2 - 3 个字节指向该字段或方法名称常量项的索引,这里是 00 07,表示指向常量池第 7 个常量所表示的信息
    • 该常量项第 4 - 5 个字节指向该字段或方法描述符常量项的索引,这里值为 00 08 表示指向常量池第 8 个常量所表示的信息
  16. 第十六个常量
    • 0c00 0500 06,是一个 CONSTANT_NameAndType_info 常量项
    • 该常量项第 2 - 3 个字节指向该字段或方法名称常量项的索引,这里是 00 07,表示指向常量池第 5 个常量所表示的信息
    • 该常量项第 4 - 5 个字节指向该字段或方法描述符常量项的索引,这里值为 00 08 表示指向常量池第 6 个常量所表示的信息
  17. 第十七个常量:01 0022 636f 6d2f 6c69 6a69 616e 6b75 6e32 342f 636c 6173 7370 7261 6374 6963 652f 4465 6d6f,是一个字符串常量,转换之后是:com/lijiankun24/classpractice/Demo
  18. 第十八个常量:0100 106a 6176 612f 6c61 6e67 2f4f 626a 6563 74,是一个字符串常量,转换之后是:java/lang/Object

通过上面的分析,我们已经手动分析完成了 18 个常量,我们也可以根据 JDK 提供的 javap 命令直接查看 .class 文件的信息,其中包括了常量池信息,如下图所示:



3.3 访问标志

在常量池结束之后,紧接着的两个字节代表访问标记(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型等。具体的标志位以及标志的含义见下表



实例:在这个例子中访问标志是:00 21,在上表中,我们并没有发现 00 21 的访问标志,这是因为在字节码文件中的访问标志,可以通过上表中多个访问标志通过或运算组成真正的访问标志。通过上表中的 ACC_SUPER 和 ACC_PUBLIC 就可以组合中 00 21 的访问标志了,也就是说该类的访问标志是 public 且允许使用 invokespecial 字节码指令的新语义的

3.4 类索引、父类索引、接口索引

在 .class 文件中由这三项数据来确定这个类的继承关系。

  1. 类索引:类索引用于确定这个类的全限定名。实例:在本例中,类索引是 00 03,表示其指向了常量池中第三个常量 —- com/lijiankun24/classpractice/Demo
  2. 父类索引:父类索引用于确定这个类的父类的全限定名。实例:在本例中,父类索引是 00 04,表示其指向了常量池中第四个常量 —- java/lang/Object
  3. 接口索引:接口索引集合用来描述类实现了哪些接口,这些被实现的接口将按照 implements 语句后的顺序从左至右排列在接口索引集合中。接口索引分为两部分,第一部分表示接口计数器(interfaces_count),是一个 u2 类型的数据,第二部分是接口索引表表示接口信息,紧跟在接口计数器之后。若一个类实现的接口为 0,则接口计数器的值为 0,接口索引表不占用任何字节。实例:在本例中,Demo 类没有实现任何接口,所以紧跟着父类索引后的两个字节是 00 00,没有接口索引表。

3.5 字段表集合

字段表集合用于描述接口或类中声明的变量。这里说的字段包括类级变量和对象级变量,但不包括方法中声明的局部变量。

字段表包括两部分:字段计数器和字段表,字段计数器表示有多少个字段,字段表的每个字段用一个名为 field_info 的表来表示,field_info 表的数据结构如下所示:



实例:本例中的字段表集合数据是:00 01 00 02 00 05 00 06 00 00

  • 在本例中,字段表中字段计数器是 00 01,在 Demo 类中有一个字段表数据,该字段表的结构如上图所示,接下来就来分析这个字段表
  • 00 01 之后是 00 02 表示该字段访问标识,表示该字段是 private 的
  • 00 02 之后是 00 05 表示该字段名称索引项,指向常量池中的第五个常量 —- m
  • 00 05 之后是 00 06 表示该字段描述符索引项,指向常量池中的第六个常量 —- I

字段表都包含的固定数据项目到 descriptor_index 为止就结束了,不过在 descriptor_index 之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。对于本例中的字段 m,它的属性表计数器为 00 00,也就是没有需要的额外描述的信息

3.6 方法表集合

在字段表之后紧跟着方法表集合,方法表表示类或接口中有几个方法。

方法表集合和上述的字段表集合几乎完全一样,最开始的 2 个字节表示一个方法计数器,在方法计数器之后,才是真正的方法数据。方法表中的每个方法都用一个 method_info 表示,其数据结构如下:



实例:在本例中方法表集合的数据是:00 0200 0100 0700 0800 0100
0900 0000 1d00 0100 0100 0000 052a b700
01b1 0000 0001 000a 0000 0006 0001 0000
0001 0001 000b 000c 0001 0009 0000 001f
0002 0001 0000 0007 2ab4 0002 0460 ac00
0000 0100 0a00 0000 0600 0100 0000 06

  1. 方法计数是 00 02,表示在 Demo 类中一共有 2 个方法,分别是编译器添加的实例构造器 和源码中的方法 inc()
  2. 第一个方法:00 01 00 07 00 08 00 01 00 09
    • access_flags:00 01 表示ACC_PUBLIC,即表示该方法是 public 的
    • name_index:00 07 表示方法名称索引项,指向常量池中的第七项 —-
    • descriptor_index:00 08 表方法描述符索引项,指向常量池中的第八项 —- ()V
    • attributes_count:00 01 表示属性计数器为 1,有 1 个属性
    • 在属性表中:
      • 第一项是 00 09 表示 attribute_name_index:指向常量池中的第九项 —- Code,说明此属性是方法的字节码描述


      • 第二项:00 00 00 1d 表示属性长度为 29 个字节
      • 第三项:00 01 00 01 00 00 00 05 2a b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003
        • 00 01:max_stack = 1
        • 00 01:max_locals = 1
        • 00 00 00 05:code_length = 5
        • 2a b700 01b1:这是 code 代码,可以通过虚拟机字节码执行进行查找
        • 2a = aload_0:将第一个引用变量推送到栈顶
        • b7 = invokespecial:调用父类构造方法
        • 00 = 什么都不做
        • 01 = 将 null 推送到栈顶
        • b1 = return: 从当前方法返回void
        • 0000:exception_table_length=0
        • 0001:attributes_count=1(Code属性表内部还含有1个属性表)
        • 000a:指向常量池中的第十个常量 —- LineNumberTable,LineNumberTable 属性结构如下图所示,内容是:0000 0006 0001 0000 0003


        • attribute_length:00 00 00 06,表示属性长度为 6
        • line_number_table_length:00 01,表示后面的 line_number_info 表有 1 个,line_number_info表包括了 start_pc 和 line_number 两个 u2 类型的数据项,前者是字节码行号,后者是 java 源码行号:start_pc:00 00,end_pc:00 03
  3. 第二个方法:0001 000b 000c 0001 0009
    • access_flags:00 01 表示ACC_PUBLIC,即表示该方法是 public 的
    • name_index:00 0b 表示方法名称索引项,指向常量池中的第十一项 —- inc
    • descriptor_index:00 0c 表方法描述符索引项,指向常量池中的第十二项 —- ()I
    • attributes_count:00 01 表示属性计数器为 1,有 1 个属性
    • 在属性表中:
      • 第一项是 00 09 表示 attribute_name_index:指向常量池中的第九项 —- Code,说明此属性是方法的字节码属性,属性内容是:0000 001f 0002 0001 0000 0007 2ab4 0002 0460 ac00 0000 0100 0a00 0000 0600 0100 0000 08
      • 第二项:00 00 00 1f 表示属性长度为 21 个字节
      • 第三项:0002 0001 0000 0007 2ab4 0002 0460 ac00 0000 0100 0a
        • 00 02:max_stack = 2
        • 00 01:max_locals = 1
        • 00 00 00 07:code_length = 7
        • 2ab4 0002 0460 ac:code
        • 00 00:exception_table_length
        • 00 01:attributes_count = 1
        • 00 0a:LineNumberTable
        • 00 0000 06:attribute_length:6
        • 00 01:line_number_table_length:1,表示后面的 line_number_info 表有 1 个,line_number_info表包括了 start_pc 和 line_number 两个 u2 类型的数据项,前者是字节码行号,后者是 java 源码行号,start_pc:00 00,end_pc:00 08

3.7 属性表集合

最后是一个属性表:00 0100 0d00 0000 0200 0e

  • 00 01:表示有 1 个attributes
  • 00 0d:指向常量池中的第十三个常量 —- SourceFile
  • 0000 0002:attribute_length=2
  • 00 0e:sourcefile_index = 指向常量池中第十四个常量 Demo.java