详解 Java 虚拟机的字节码 —— Class 文件的结构

详解 Java 虚拟机的字节码 —— Class 文件的结构

  • 一   Java 虚拟机
  • 二   Class 文件
    • 2.1 Class 文件的结构
    • 2.2 Magic
    • 2.3 Version
    • 2.4 Constant Pool
    • 2.5 Access Flags
    • 2.6 This Class
    • 2.7 Super Class
    • 2.8 Interfaces
    • 2.9 Fields
    • 2.10 Methods
      • 2.10.1 Method 1
      • 2.10.2 Method 2
      • 2.10.3 Method 3
    • 2.11 Attributes
  • 三   结语

本文所有图片均为 1920 × 1080 的高清图片,如果看不清,请点击放大哦!

一   Java 虚拟机

相信你在阅读这篇文章之前,一定或多或少对 Java 有所了解,你肯定经常听到 “ Java 是一门跨平台的语言” 、 “ Java 程序由 Java 虚拟机负责执行” 之类的话。

我们会选择一个开发环境来编写 Java 代码,例如我们使用 IntelliJ IDEA 编写了一个类文件 Demo.java ,点击 “运行” 按钮就可以执行该文件。但是,如果没有 IntelliJ IDEA 之类的开发环境,我们应该如何编译并运行 Java 程序呢?

如图 1.1 所示,我们编写了一个类文件 Demo.java ,当然,你可以使用任何文本编辑软件编写,例如 IntelliJ IDEAEclipseVimNotepad 等都可以, Demo.java 称为源文件。接下来,我们需要使用 Java 编译器编译源文件,使用 javac Demo.java 命令即可编译源文件 Demo.java ,并生成字节码文件 Demo.class 。注意,如果你的源文件里有静态错误, Java 编译器会报错,这时你需要修改源文件再重新编译,直到 Java 编译器检查到源文件没有错误,才能完成编译并生成 .class 字节码文件。

我们可以使用 javap Demo.class 命令来反编译字节码文件。该命令将字节码文件的二进制数据格式化成我们能读懂的文本信息。本文的目的就是解析 .class 字节码文件的结构,在读完本文后,你可以胜任 javap 命令的任务,成为一个 “手动 Java 反编译器” ,嘿嘿!

我们可以使用 java Demo 命令运行,注意这里不需要加后缀名 .class 了,效果和点击开发环境中的 “运行” 按钮是一样的。

如果你有多个 .class 文件,可以使用 jar 命令将这些字节码文件打包成一个 .jar 包文件(JAR,Java Archive File),我们依然可以使用 java 命令来运行 .jar 包文件。 .jar 包文件的好处是有利于网络传输和服务器部署,例如 Minecraft 的服务器程序就打包成了一个 .jar 包文件。你可以将 .jar 包文件理解成一个文件夹,里面包含了很多的 .class 字节码文件。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第1张图片

图 1.1   Java 程序的编译、反编译、打包、运行

当你编译生成了一个 .class 字节码文件,或者打包了一个 .jar 包文件,你就可以在任何安装了 Java 虚拟机的设备上运行。如图 1.2 所示,你可以在包括但不限于 Windows 、 Linux 、 Mac 、Android 等平台上运行 .class.jar 文件。也就是说, Java 虚拟机(JVM,Java Virtual Machine)解决了系统层面的差异,并为 Java 程序提供了一个统一的运行平台。每个操作系统都有一套自己的 API 接口,我们需要根据不同的操作系统,去调用相应的接口。但是 JVM 替我们解决了不同操作系统的接口差异,并提供了统一的 Java 接口。我们在编写 Java 程序时,只要调用这些 Java 接口就行了。所以说 “ Java 是一门跨平台的语言” 。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第2张图片

图 1.2   Java 虚拟机

想要深入了解 JVM 的运行原理,你可以访问 Java SE Specifications ,下载 Oracle 编写的 Java 语言和 Java 虚拟机的官方文档。如图 1.3 所示,是 Java 14 虚拟机的官方文档(The Java Virtual Machine Specification, Java SE 14 Edition)。

图 1.3   JVM 官方文档

二   Class 文件

我们以下面的 Java 代码为例,详细阐述 .class 文件的结构,我们将下面的代码写入文件 Demo.java ,并使用 javac Demo.java 命令编译生成字节码文件 Demo.class

public class Demo {
    private int n = 5;

    private int add(int a, int b) {
        int c = a + b;
        return c;
    }

    protected int minus(int a, int b) {
        int c = a - b;
        return c;
    }
}

此时,我们可以用 javap -v -p Demo.class 命令反编译 Demo.class ,其中参数 -v -verbose 表示输出附加信息, -p -private 表示输出所有的类和成员,否则默认不输出私有的类和成员。如果不加 -p 参数,则不会输出私有的 add() 方法。输出的内容如下所示,这就是我们最终解析出来的内容。内容有点长,不过不要怕,掌握了方法就很简单了,加油!

Classfile /home/administrator/Projects/IdeaProjects/src/Demo.class
  Last modified Dec 15, 2020; size 335 bytes
  SHA-256 checksum 0f05381de59579fe48b4a25541383c27a7941dc40d581599ba7c105474c13417
  Compiled from "Demo.java"
public class Demo
  minor version: 0
  major version: 58
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // Demo
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // Demo.n:I
   #8 = Class              #10            // Demo
   #9 = NameAndType        #11:#12        // n:I
  #10 = Utf8               Demo
  #11 = Utf8               n
  #12 = Utf8               I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               add
  #16 = Utf8               (II)I
  #17 = Utf8               minus
  #18 = Utf8               SourceFile
  #19 = Utf8               Demo.java
{
  private int n;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE

  public Demo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: iconst_5
         6: putfield      #7                  // Field n:I
         9: return
      LineNumberTable:
        line 1: 0
        line 2: 4

  private int add(int, int);
    descriptor: (II)I
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=2, locals=4, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: istore_3
         4: iload_3
         5: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 4

  protected int minus(int, int);
    descriptor: (II)I
    flags: (0x0004) ACC_PROTECTED
    Code:
      stack=2, locals=4, args_size=3
         0: iload_1
         1: iload_2
         2: isub
         3: istore_3
         4: iload_3
         5: ireturn
      LineNumberTable:
        line 10: 0
        line 11: 4
}
SourceFile: "Demo.java"

下面我们就正式开始解析 Demo.class 文件啦!首先我们要以二进制形式打开该文件。这里我使用 vim ,在命令模式下输入 %!xxd 回车就能以二进制形式显示文件了。

2.1 Class 文件的结构

如图 2.1.1 所示,左边就是以二进制形式显示的 Demo.class 文件内容。 .class 文件一共分为 10 个版块,分别是 MagicVersionConstant PoolAccess FlagsThis ClassSuper ClassInterfacesFieldsMethodsAttributes 。从各个版块所占的篇幅可以看出, Constant PoolMethods 是两个大头。接下来,我们将详细解析每个版块。

你可以访问 JVM 官方文档 Chapter 4. The class File Format ,以获取更多信息。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第3张图片

图 2.1.1   Class 文件的结构

2.2 Magic

Magic(魔数)是一个常量,如图 2.2.1 所示,其值恒为 0x CAFE BABE 。魔数用来检验一个文件是否满足 .class 字节码文件的格式。虽然我们可以直接通过文件后缀名来检验,但 Java 并不强制文件后缀名一定要是 .class 或者 .jar ,所以魔数提供了一道保障。

我们再来看看魔数的值,把 0x CAFE BABE 最后的 E 改成 Y ,就变成了 CAFE BABY ,翻译一下就是 “咖啡宝宝” 。嘿嘿,再看看 Java 的图标,是不是一杯咖啡呢?感觉这是一种属于程序员的浪漫(Programmers’ Romance)。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第4张图片

图 2.2.1   Magic

2.3 Version

Version(版本)标识了 .class 文件的版本。如图 2.3.1 所示,该文件的版本是 58.0 ,查看对照表,发现是 Java SE 14 所对应的字节码文件。嗯,确实是我使用的 Java 版本。

Version

图 2.3.1   Version

2.4 Constant Pool

Constant Pool(常量池)存储了该文件中的所有常量。如图 2.4.1 所示, constant_pool_count 的值为 20 ,但注意看图中红框内的第二行,实际的常量个数是 constant_pool-count-1 ,也就是说常量池中一共有 19 个常量。图中右上角列出了 cp_info 的格式,可以看到,所有的常量都以一个 tag 开头,后面若干字符表示该常量的信息。下面我们来逐个解析这 19 个常量。

你可以访问 JVM 官方文档 4.4. The Constant Pool ,以获取更多信息。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第5张图片

图 2.4.1   Constant Pool Count

如图 2.4.2 所示,第一个常量的 tag = 0x0a = 10 ,查看 tag 对照表,发现该常量的类型是 CONSTANT_Methodref ,该类型的常量描述一个方法。查看该类型对应的结构,其有一个 class_index 值为 2 ,一个 name_and_type_index 值为 3 。以 index 结尾的项都是引用,即引用对应编号的常量。这里表示引用第 2 个常量和第 3 个常量。

第二个常量的 tag = 0x07 = 7 ,查看 tag 对照表,发现该常量的类型是 CONSTANT_Class ,该类型的常量描述一个类或一个借口。查看该类型对应的结构,其只有一个 name_index 值为 4 ,表示引用第 4 个常量。

第三个常量的 tag = 0x0c = 12 ,查看 tag 对照表,发现该常量的类型是 CONSTANT_NameAndType ,该类型的常量描述一个变量或一个方法,但不会记录其属于哪个类或哪个借口。查看该类型对应的结构,其有一个 name_index 值为 5 ,表示引用第 5 个常量,一个 descriptor_index 值为 6 ,表示引用第 6 个常量。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第6张图片

图 2.4.2   Constant Pool #1, #2, #3

如图 2.4.3 所示,第四个常量的 tag = 0x01 = 1 ,查看 tag 对照表,发现该常量的类型是 CONSTANT_Utf8 ,该类型的常量存储一个字符串。查看该类型对应的结构,其有一个 length 值为 16 ,表示该字符串的长度为 16 。字符串的值为 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 ,这是采用 UTF-8 编码的字符串, UTF-8 的英文编码和 ASCII 相同,通过 ASCII 表,我们解析出该字符串为 java/lang/Object

第五个常量和第六个常量,也都是 CONSTANT_Utf8 常量,解析过程就不赘述了,详见第四个常量的解析。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第7张图片

图 2.4.3   Constant Pool #4, #5, #6

如图 2.4.4 所示,第七个常量的 tag = 0x09 = 9 ,查看 tag 对照表,发现该常量的类型是 CONSTANT_Fieldref ,该类型的常量描述一个成员变量。查看该类型对应的结构,其有一个 class_index 值为 8 ,一个 name_and_type_index 值为 9

第八个常量,是 Constant_Class 常量,解析过程就不赘述了,详见第二个常量的解析。

第九个常量,是 Constant_NameAndType 常量,解析过程就不赘述了,详见第三个常量的解析。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第8张图片

图 2.4.4   Constant Pool #7, #8, #9

图 2.4.5 中的第十个常量、第十一个常量、第十二个常量,图 2.4.6 中的第十三个常量、第十四个常量、第十五个常量,图 2.4.7 中的第十六个常量、第十七个常量、第十八个常量、第十九个常量,都是 CONSTANT_Utf8 常量,解析过程就不赘述了,详见第四个常量的解析。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第9张图片

图 2.4.5   Constant Pool #10, #11, #12

详解 Java 虚拟机的字节码 —— Class 文件的结构_第10张图片

图 2.4.6   Constant Pool #13, #14, #15

详解 Java 虚拟机的字节码 —— Class 文件的结构_第11张图片

图 2.4.7   Constant Pool #16, #17, #18, #19

至此,我们解析出了所有的 19 个常量,我们将这 19 个常量全部列出来,如图 2.4.8 所示。在接下来的解析过程中,我们会反复用到这些常量。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第12张图片

图 2.4.8   Constant Pool #1 - #19


2.5 Access Flags

Access Flags(访问标志)标识该类或接口的访问权限。如图 2.5.1 所示,该类访问标志的值是 0x0021 ,查看对照表,发现 0x00210x00200x0001 的和,表明该类拥有 ACC_SUPER 访问权限,该权限允许外部使用 invokespecial 指令调用该类,该类还拥有 ACC_PUBLIC 访问权限,该权限声明该类是公有的,外部可以访问该类。

事实上,我们仔细观察每个访问权限的数值,其只用了四位中的一位,且都是 2 的幂次,所以,任何一个访问标志都有唯一分解。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第13张图片

图 2.5.1   Access Flags

2.6 This Class

This Class(当前类)标识当前类的名称,是一个指向常量池的引用。如图 2.6.1 所示,其值为 8 ,表示指向第八个常量。第八个常量描述一个类或一个接口,又指向第十个常量。第十个常量存储一个字符串 Demo ,这正是我们这个类的类名。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第14张图片

图 2.6.1   This Class

2.7 Super Class

Super Class(父类)标识父类的名称,是一个指向常量池的引用。由于 Java 不允许多重继承,所以当前类所继承的父类有且只有一个。和 This Class 的解析类似,如图 2.7.1 所示,其值为 2 ,表示指向第二个常量。第二个常量描述一个类或一个接口,又指向第四个常量。第四个常量存储一个字符串 java/lang/Object ,这是 Java 的基类,所有类都是 Object 基类的直接或间接子类。由于我们定义的 Demo 类没有显式继承任何类,所以直接继承自 Ojbect 基类。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第15张图片

图 2.7.1   Super Class

2.8 Interfaces

Interfaces(接口)存储了该文件中的所有接口。由于 Java 允许继承多个接口,所以存储的是一个数组,数组中的每一项都是一个指向常量池的引用,表示一个接口。如图 2.8.1 所示, interfaces_count 的值为 0 ,也就是说当前文件没有定义接口。如果 interfaces_count 的值大于 0 ,则后面会存储 interfaces_count 个接口引用。每个接口引用占两个字节,都指向常量池中的一个常量。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第16张图片

图 2.8.1   Interfaces

2.9 Fields

Fields(成员变量)存储了该文件中的所有成员变量。由于成员变量有多个,所以存储的是一个数组,数组中的每一项都是一个指向常量池的引用,表示一个成员变量。如图 2.9.1 所示, fields_count 的值为 1 ,表示有 1 个成员变量。

你可以访问 JVM 官方文档 4.5. Fields ,以获取更多信息。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第17张图片

图 2.9.1   Fields Count

如图 2.9.2 所示,该成员变量有一个 access_flags 值为 2 ,查看对照表,发现访问标志是 ACC_PRIVATE ,表示该成员变量是一个私有变量,只能在类内访问;有一个 name_index 值为 11 ,而第 11 个常量的值为 n ,表示该成员变量的变量名是 n ;有一个 descriptor_index 值为 12 ,而第 12 个常量的值为 I ,表示该成员变量的类型为 int ;有一个 attributes_count 的值为 0 ,表明该成员变量没有附加属性。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第18张图片

图 2.9.2   Fields

2.10 Methods

Methods(方法)存储了该文件中的所有方法。如图 2.10.1 所示, methods_count 的值为 3 ,说明该文件中有 3 个方法。下面,我们以第一个方法为例,详细解析第一个方法,而简要叙述第二和第三个方法的解析结果。

你可以访问 JVM 官方文档 4.6. Methods ,以获取更多信息。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第19张图片

图 2.10.1   Methods Count

2.10.1 Method 1

下面我们来解析第一个方法。如图 2.10.2 所示,其有一个 access_flags 值为 1 ,查看对照表,发现访问标志是 ACC_PUBLIC ,表示该方法是一个公有方法,任何类均能调用该方法;有一个 name_index 值为 5 ,而第 5 个常量的值为 ,表示该方法是该类的构造函数;有一个 descriptor_index 值为 6 ,而第 6 个常量的值为 ()V ,表示该方法既没有输入参数,也没有返回值;有一个 attributes_count 值为 1 ,表示该方法有一个属性。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第20张图片

图 2.10.2   Method_1

如图 2.10.3 所示,所有的属性都遵循 attribute_info 的结构。该属性有一个 attribute_name_index 值为 13 ,而第 13 个常量的值为 Code ,表示该属性是一个 Code 属性;有一个 attribute_length 值为 38 ,表示该属性还要占用 38 个字节,注意这个长度不包括前两项 6 字节的长度。我们找到 Code 属性的结构 Code_attribute ,继续解析该属性。

你可以访问 JVM 官方文档 4.7. Attributes ,以获取更多信息。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第21张图片

图 2.10.3   Code_Attribute of Method_1

如图 2.10.4 所示,该属性有一个 max_stack 值为 2 ,表示该方法在运行过程中,操作栈的最大深度是 2 ;有一个 max_locals 值为 1 ,表示该方法有 1 个局部变量;有一个 code_length 值为 10 ,表示接下来的 10 个字节都是指令,其指令是 2a b7 00 01 2a 08 b5 00 07 b1

详解 Java 虚拟机的字节码 —— Class 文件的结构_第22张图片

图 2.10.4   Code_Attribute of Method_1

下面我们来解析这串指令 2a b7 00 01 2a 08 b5 00 07 b1 。指令一条一条顺序排列,每个指令用 1 个字节表示,有些指令不带参数,而有些指令带一个或多个参数。

如图 2.10.5 所示,第一个字节是 2a ,对应的是 aload_0 指令,不带参数,该指令的含义是将第 0 个局部变量压入操作栈;接下来的一个字节是 b7 ,对应的是 invokespecial 指令,带两个参数 00 01 ,该参数表示第 1 个常量 java/lang/Object.“”:()V ,该指令的含义是直接调用该类的构造函数;接下来的一个字节是 2a ,对应的仍是 aload_0 指令,不带参数。

你可以访问 JVM 官方文档 Chapter 6. The Java Virtual Machine Instruction Set ,以获取更多信息。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第23张图片

图 2.10.5   Instructions of Code_Attribute of Method_1

如图 2.10.6 所示,接下来的一个字节是 08 ,对应的是 iconst_5 指令,不带参数,该指令的含义是
将整形常量 5 压入操作栈;接下来的一个字节是 b5 ,对应的是 putfield 指令,带两个参数 00 07 ,该参数表示第 7 个常量 Demo.n:I ,该指令的含义是将成员变量 Demo.n 的值置为 5 ,这里的值 5 是上一条指令 iconst_5 压入操作栈的;接下来的一个字节是 b1 ,对应的是 return 指令,不带参数,该指令的含义是退出方法并返回空值 void

详解 Java 虚拟机的字节码 —— Class 文件的结构_第24张图片

图 2.10.6   Instructions of Code_Attribute of Method_1

好了,解析完了这串指令,我们继续来解析 Code_attribute 。如图 2.10.7 所示,该属性有一个 exception_table_length 值为 0 ,表示该属性没有异常表,因为我们没有写 try ... catch ... 语句;有一个 attributes_count 值为 1 ,表示该属性还有一个附加属性,开始套娃了,嘿嘿!这个附加属性是 00 0e 00 ... 02

详解 Java 虚拟机的字节码 —— Class 文件的结构_第25张图片

图 2.10.7   Code_Attribute of Method_1

如图 2.10.8 所示,这个附加属性有一个 attribute_name_index 值为 14 ,而第 14 个常量的值为 LineNumberTable ,表示该属性是一个 LineNumberTable 属性;有一个 attribute_length 值为 10 ,表示该属性还要占用 10 个字节,注意这个长度不包括前两项 6 字节的长度,这个我们之前说过。我们找到 LineNumberTable 属性的结构 LineNumberTable_attribute ,继续解析该属性。

该属性有一个 line_number_table_length 值为 2 ,说明行号表中有两条记录。第一条记录是 00 00 00 01 ,其中前两个字节 00 00 表示该方法中指令的编号,注意第 n 号指令是指从第 n 个字节开始的那条指令,后两个字节 00 01 表示源文件 Demo.java 中的行号。第一条记录表示该方法的第 0 号指令 aload_0 对应源文件中的第 1 行 public class Demo { 。同理,第二条记录表示该方法的第 4 号指令 aload_0 对应源文件中的第 2 行 private int n = 5; 。该属性的目的是为了在执行指令中遇到错误后,定位到源文件中的某行语句,方便我们查错。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第26张图片

图 2.10.8   LineNumberTable_attribute of Code_Attribute of Method_1

好了,至此,我们就完整地解析了第一个方法的所有内容。是不是觉得自己很了不起呢!给自己鼓鼓掌吧!如图 2.10.9 所示,我们将第一个方法的内容全部写出来了。细心的读者肯定已经注意到了,这就是我们用 javap 命令输出的内容中,有关第一个方法的部分。熟悉 Java 的读者肯定知道,这个方法是该类的构造函数,但我们并没有在 Demo.java 源文件中显式地定义构造函数,这个构造函数是编译器自动为我们加上的。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第27张图片

图 2.10.9   Results of Method_1

2.10.2 Method 2

第二个方法的解析过程,就不写详细的文字描述了。相信你在理解了第一个方法解析过程的基础上,已经完全可以看懂第二个方法是如何解析的了。从图 2.10.10 到图 2.10.16 ,都是第二个方法的解析过程。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第28张图片

图 2.10.10   Method_2

详解 Java 虚拟机的字节码 —— Class 文件的结构_第29张图片

图 2.10.11   Code_Attribute of Method_2

详解 Java 虚拟机的字节码 —— Class 文件的结构_第30张图片

图 2.10.12   Code_Attribute of Method_2

详解 Java 虚拟机的字节码 —— Class 文件的结构_第31张图片

图 2.10.13   Instructions of Code_Attribute of Method_2

详解 Java 虚拟机的字节码 —— Class 文件的结构_第32张图片

图 2.10.14   Code_Attribute of Method_2

详解 Java 虚拟机的字节码 —— Class 文件的结构_第33张图片

图 2.10.15   LineNumberTable_attribute of Code_Attribute of Method_2

详解 Java 虚拟机的字节码 —— Class 文件的结构_第34张图片

图 2.10.16   Result of Method_2

2.10.3 Method 3

和第二个方法一样,第三个方法也不写详细的文字描述了。从图 2.10.17 到图 2.10.23 ,都是第三个方法的解析过程。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第35张图片

图 2.10.17   Method_3

详解 Java 虚拟机的字节码 —— Class 文件的结构_第36张图片

图 2.10.18   Code_Attribute of Method_3

详解 Java 虚拟机的字节码 —— Class 文件的结构_第37张图片

图 2.10.19   Code_Attribute of Method_3

详解 Java 虚拟机的字节码 —— Class 文件的结构_第38张图片

图 2.10.20   Instructions of Code_Attribute of Method_3

详解 Java 虚拟机的字节码 —— Class 文件的结构_第39张图片

图 2.10.21   Code_Attribute of Method_3

详解 Java 虚拟机的字节码 —— Class 文件的结构_第40张图片

图 2.10.22   LineNumberTable_attribute of Code_Attribute of Method_3

详解 Java 虚拟机的字节码 —— Class 文件的结构_第41张图片

图 2.10.23   Result of Method_3

2.11 Attributes

Attributes(属性)存储了该文件的所有属性。如图 2.11.1 所示, attributes_count 的值为 1 ,也就是说当前文件有一个属性。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第42张图片

图 2.11.1   Attributes Count

如图 2.11.2 所示,该属性有一个 attribute_name_index 值为 18 ,而第 18 个常量的值为 SourceFile ,表示该属性是一个 SourceFile 属性;有一个 attribute_length 值为 2 ,表示该属性还要占用 2 个字节,注意这个长度不包括前两项 6 字节的长度,这个我们之前说过两次啦!我们找到 SourceFile 属性的结构 SourceFile_attribute 。该属性还有一个 sourcefile_index 值为 19 ,而第 19 个常量的值为 Demo.java 。该属性表示本文件 Demo.class 是由 Demo.java 源文件编译而来的。

详解 Java 虚拟机的字节码 —— Class 文件的结构_第43张图片

图 2.11.2   Attributes #1

三   结语

亲爱的读者,非常高兴能在这里与你见面!我相信,如果你认认真真看完了这篇文章,你一定已经掌握了解析 .class 文件的能力。其实说白了,解析的过程就是根据结构体的定义,将二进制数据分段,再根据说明和对照表,解释每一段数据的含义。例如,整个 .class 文件就是一个大的 ClassFile 结构体,根据定义,前 4 个字节是一段,称为 magic ,再根据说明解释其含义。又例如, access_flags 需要查看对照表,来解释其含义。

本文不可能覆盖 .class 文件中所有的结构体和 JVM 的所有指令。想要获取完整信息,你可以访问 Java SE Specifications ,下载 Oracle 编写的 Java 语言和 Java 虚拟机的官方文档,里面有非常详细的说明。当然,六百页的篇幅也是非常感人的,足以劝退很多人了!

相信细心的读者已经注意到了,文件最后有一个字节 0a ,我没有解释其含义。因为我翻阅了 Java 虚拟机的官方文档,里面没有对这种情况的说明。而且,在最后的 0a 这个字节之前,整个文件已经被完整地解析了,所以这个字节可有可无,不会对解析产生任何影响。当然,也有可能是我用 vim 查看的方式有问题?希望有知道原因的读者给我留言,我不胜感激!

最后,希望我写的文章能对您起到一点儿帮助,您的肯定是我最大的快乐!

你可能感兴趣的