JVM Bytecode

分析字节码的步骤:

  1. 预备好字节码文件参照表,这里有定义好的一些结构,比如字面量、常量池类型和它相关的数据结构、接口相关的数据结构、类相关的等等,可以找官方的 Java Virtual Machine Specification / The class file format.

  2. 编译源码得到 class 文件,用 16 进制查看器可以看到二进制内容

  3. 根据字节码文件的结构定义,按顺序进行分析,不懂的就查手册

字节码

字节码是 JVM 虚拟机规范的一部分,它是 Java 实现一次编译到处运行的关键,它连接了 Java 源码和机器码,所以掌握字节码对于理解 JVM 虚拟机的运行机制非常有帮助。

在 JVM 的虚拟机规范中有关于字节码类文件的定义,参看 Java Virtual Machine Specification / The class file format. 字节码文件结构是一组以 8 字节为基础的二进制流,各数据严格按照顺序紧凑的排列在文件中,中间并没有特殊的分隔符,如何识别不同的组(或者说不同的部分)?使用的是长度标识的方法。下面是字节码文件结构的大致组成部分:

字节码文件组成部分

通过一段程序来分析:

将上面这段代码编译后,会产生 3 个 .class 文件,分别是:BytecodeTest.class, BytecodeReader.class, Reader.class,当我们运行 BytecodeTest.java 中的 main 方法时,JVM 的类加载系统会把这三个字节码文件都加载到内存的方法区(这个区域在 1.8 及以后被叫做元空间 Metaspace),类加载的过程也是读取字节码文件,然后在内存中构建 Java 可使用的对象的过程。

来详细分析一下这些字节码文件,分析最复杂的 BytecodeTest.class 文件,用 16 进制查看器查看这个文件如下:

上面有一张字节码文件的大致组成部分的图,依次来分析

  • 最开始的前 4 个字节,是魔数,用来快速判断这是不是一个 Java 的字节码文件,如果不是直接退出或者报错;CAFEBABE 这 4 个字节是 Java 的魔数

  • 紧接着的 4 个字节 0000 0034 , 前两个字节是此版本号,后两个字节是主版本号,0034 表示是 JDK 1.8

  • 接着就是常量池的部分,由常量池个数和常量池表组成

    • 第 9~10 这 2 字节表示常量池中常量的个数,这里是 0036 ,10 进制是 54,那么常量池表中的数据项就是 53 个,为啥需要 -1,因为常量池中的第0个位置被我们的jvm占用了表示为null 所以我们通过编译出来的常量池索引是从1开始的

    • 紧接着后面的就是常量池表了,逐项分析,在这个常量池表中的每一项都以1个字节长度的 tag 来标记常量的类型

    • 常量池表中的第 1 项,0A 表示 CONSTANT_Methodref_info, 这种类型的常量后面有 2 字节的类索引 (CONSTANT_Class_info)和 2 字节的名称及类型描述符(CONSTANT_NameAndType_info ); 所以 00 0E 是 14,表示指向常量池中位置是 14 的常量; 00 1E 是 30, 表示指向常量池中位置是 30 的常量,而且这个常量的类型是 NameAndType_info

    • 第二个常量 07 00 1F, 07 表示 CONSTANT_Class_info , 接着的 2 个字节指向权限定类名,指向索引 31,索引 31 处其实是 31 = Utf8 io/github/shniu/toolbox/BytecodeReader , 它是一个 Utf8 的字面量

    • 第三个常量 0A 00 02 00 1E,表示是一个 CONSTANT_Methodref_info , 分别指向索引 2 和 30,也就是指向 BytecodeReader 的无参构造方法

    • 以此类推进行分析即可

  • 常量池后紧接着是访问标志符号,2 个字节,我们的文件里是 00 21 ,表示是 0x0021 是通过位运算 & 计算出来的,这个 class 的访问权限是 ACC_PUBLICACC_SUPER

  • 紧接着是 this class name, 表示当前所属类,2 个字节,值为 00 05 ,表示索引指向为 5 的常量池中的元素,#5 位置是一个 class,指向了 BytecodeTest

  • 再接着是 super class name, 表示当前类继承的父类名称,占用 2 个字节,值为 00 0F ,表示指向 #15,而这个位置是 Object 类,也就是当前类的父类是 Object

  • 再接着是接口的相关信息,2 个字节表示接口数量,后面跟着的是接口表;接口数量是 00 00 ,说明当前类没有实现接口;如果当前类实现了接口,比如代码中的 BytecodeReader 类实现了一个接口 Reader ,那么这里的值就是 00 01 ,紧接着的 2 个字节就是该接口指向的常量池中的位置,为 00 07 ,#7 指向的是 Class_info

  • 接口信息之后是字段表的信息,它表示的是类变量或者实例变量,但不包括方法的局部变量,接着的 2 个字节是字段的个数,值为 00 02

    • 每个字段表按如下方式组成

    • `00 1A 00 10 00 11 00 01 00 12 00 00

字段的数据结构
  • 方法表

00 03 表示有 3 个方法,在我们的类中是 无参构造方法、startup 方法、main 方法

  • 最后是 .class 文件属性: 00 01 00 1D 00 00 00 02 00 1E , 表示有 1(00 01) 个属性,属性指向的位置是 00 1D,也就是 #29,属性的长度是 00 00 00 02 也就是长度为 2,00 1E 就是属性的值,指向 #30

文件属性结构

字节码文件内容如下

LineNumberTable属性

LineNumberTable是Code属性中的一个子属性,用来描述java源文件行号与字节码文件偏移量之间的对应关系。当程序运行抛出异常时,异常堆栈中显示出错的行号就是根据这个对应关系来显示的

总结

字节码组织结构
常量池类型分类

Reference

最后更新于

这有帮助吗?