JVM字节码分析

字节码实战

    • 字节码实战
    • 一、字节码
    • 二、分析字节码
      • 1. 魔数
      • 2. 版本号
      • 3. 常量池
        • 3.1. 常量池基本结构
        • 3.2. JVM所定义的11中常量
        • 3.3. 常量池元素的复合结构
        • 3.4. 常量池第一个元素分析
        • 3.5. 变量型常量池分析
      • 4. 访问标识和继承信息
        • 4.1. access_flags
        • 4.2. this_class
        • 4.3. super_class
        • 4.4. interface
          • 4.4.1 interfaces_count
          • 4.4.2 interfaces
      • 5. 字段信息
        • 5.1. fields_counts
        • 5.2. fields_info fields
          • 5.3. fields结构组成格式
          • 5.3.1. 变量a分析
          • 5.3.2. 变量si和s
      • 6. 方法信息
        • 6.1 methods_count
        • 6.2 methods_info methods
          • 6.2.1. methods结构组成格式
          • 6.2.2. 第一个方法void()
          • 6.2.3. 第二个方法void()
    • 三、附件

字节码实战

一、字节码

  1. 创建 Test.java 类
public class Test {
    public int a = 3;
    static Integer si = 6;
    String s = "Hello World!";

    public static void main (String arg[]){
        Test test = new Test();
        test.a = 8;
        si = 9;
    }

    public void test(){
        this.a = a;
    }
}
  1. 通过 javac 将 Test.java 编译成 Test.class
  2. 通过命令 javap -verbose Test 获取 Test.class 对应的字节码
Classfile /Users/zhishengjie/workspace/learning/vm/Test.class
  Last modified 2021-7-13; size 627 bytes
  MD5 checksum 7122e264980126ec0b9f4c7b76021ffe
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#26         // java/lang/Object."":()V
   #2 = Fieldref           #5.#27         // Test.a:I
   #3 = String             #28            // Hello World!
   #4 = Fieldref           #5.#29         // Test.s:Ljava/lang/String;
   #5 = Class              #30            // Test
   #6 = Methodref          #5.#26         // Test."":()V
   #7 = Methodref          #31.#32        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #8 = Fieldref           #5.#33         // Test.si:Ljava/lang/Integer;
   #9 = Class              #34            // java/lang/Object
  #10 = Utf8               a
  #11 = Utf8               I
  #12 = Utf8               si
  #13 = Utf8               Ljava/lang/Integer;
  #14 = Utf8               s
  #15 = Utf8               Ljava/lang/String;
  #16 = Utf8               
  #17 = Utf8               ()V
  #18 = Utf8               Code
  #19 = Utf8               LineNumberTable
  #20 = Utf8               main
  #21 = Utf8               ([Ljava/lang/String;)V
  #22 = Utf8               test
  #23 = Utf8               
  #24 = Utf8               SourceFile
  #25 = Utf8               Test.java
  #26 = NameAndType        #16:#17        // "":()V
  #27 = NameAndType        #10:#11        // a:I
  #28 = Utf8               Hello World!
  #29 = NameAndType        #14:#15        // s:Ljava/lang/String;
  #30 = Utf8               Test
  #31 = Class              #35            // java/lang/Integer
  #32 = NameAndType        #36:#37        // valueOf:(I)Ljava/lang/Integer;
  #33 = NameAndType        #12:#13        // si:Ljava/lang/Integer;
  #34 = Utf8               java/lang/Object
  #35 = Utf8               java/lang/Integer
  #36 = Utf8               valueOf
  #37 = Utf8               (I)Ljava/lang/Integer;
{
  public int a;
    descriptor: I
    flags: ACC_PUBLIC

  static java.lang.Integer si;
    descriptor: Ljava/lang/Integer;
    flags: ACC_STATIC

  java.lang.String s;
    descriptor: Ljava/lang/String;
    flags:

  public Test();
    descriptor: ()V
    flags: 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_3
         6: putfield      #2                  // Field a:I
         9: aload_0
        10: ldc           #3                  // String Hello World!
        12: putfield      #4                  // Field s:Ljava/lang/String;
        15: return
      LineNumberTable:
        line 1: 0
        line 2: 4
        line 4: 9

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #5                  // class Test
         3: dup
         4: invokespecial #6                  // Method "":()V
         7: astore_1
         8: aload_1
         9: bipush        8
        11: putfield      #2                  // Field a:I
        14: bipush        9
        16: invokestatic  #7                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        19: putstatic     #8                  // Field si:Ljava/lang/Integer;
        22: return
      LineNumberTable:
        line 7: 0
        line 8: 8
        line 9: 14
        line 10: 22

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: aload_0
         2: getfield      #2                  // Field a:I
         5: putfield      #2                  // Field a:I
         8: return
      LineNumberTable:
        line 13: 0
        line 14: 8

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        6
         2: invokestatic  #7                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: putstatic     #8                  // Field si:Ljava/lang/Integer;
         8: return
      LineNumberTable:
        line 3: 0
}
SourceFile: "Test.java"

值得注意的是:使用 javap -verbose 命令分析一个字节码文件时,将会分析字节码文件的魔数、版本号、常量池、类信息、类的构造函数、类中所包含的方法信息以及类(成员)变量信息。需要注意的是,每一次执行 javap 命令所输出的信息内容一定是相同的,但是信息的先后顺序则不保证完全一致,例如,常量池中的元素编号每次都不保证相同。

  1. 查看字节码二进制,使用十六进制工具打开 Test.class
    在这里插入图片描述

二、分析字节码

1. 魔数

所有 .class 字节码文件的开始四个字节都是 魔数 ,并且其值一定是 0xCAFEBABE ,这里的 CAFEBABE 是指十六进制数值,并不是字符串 “CAFEBABE”,如果开始的4个字节不是 0xCAFEBABE ,则JVM将会认为该文件不是 .class 字节码文件,并拒绝解析。
在这里插入图片描述
读者可以根据左侧的行号进行定位,图中选中的部分就是 魔数

2. 版本号

根据字节码文件规范,魔数之后的四个字节为版本信息,前两个字节表示 major version ,即主版本号;后面两个字节为 minor version ,即次版本号。
这里版本号的值为 0x00000034 对应的十进制数是 52 。目前已知发布的version:1.1(45)、1.2(46)、1.3(47)、1.4(48)、1.5(49)、1.6(50)、1.7(51)、1.8(52)。据此可以知道,该 class 文件是 JDK1.8 编译的。
在这里插入图片描述

3. 常量池

常量池是 .class 字节码文件中非常重要和核心的内容,一个java类中的绝大多数的信息都是有常量池描述的,尤其是java类中定义的变量和方法,都是常量池保存的。注意,对 JVM 所有研究的人,可能都知道 JVM 的内存模型中,有一块就是常量池,JVM堆栈的常量池就是用于保存每一个 Java 类所对应的常量池的信息的,一个java应用程序中所包含的所有Java类的常量池组成了jvm堆区中大的常量池。

3.1. 常量池基本结构

Java类所对应的常量池主要由常量池数量和常量池数组两部分组成,常量池数量紧跟在次版本号的后面,占2字节。常量池数组则紧随在常量池数量之后。
常量池数组,顾名思义,就是一个类似数组的结构。这个数组固化在字节码文件中,由多个元素组成。但是每一个元素的第一个数据都是u1类型,该字节是标志位,占一个字节。JVM解析常量池时,根据u1类型来获取该元素的具体类型。常量池的组成结构如下图所示。
JVM字节码分析_第1张图片

3.2. JVM所定义的11中常量

常量池元素中的不同元素结构与类型都是不同的,正因为如此,JVM只能定义有限的元素类型,并针对有限的类型进行专门解析。JVM一共定义了11中常量,如下图所示。

可以看到,类的方法信息、接口和继承信息、属性信息都是定义在NamedAndType_Info中的,关于该结构,下文会详细讲解。

3.3. 常量池元素的复合结构

常量池数组中的每一种元素的内容都是复合数据结构的,下面分别给出jvm所定义的常量池中的每一种元素的具体结构。

举例说明:
JVM字节码分析_第2张图片
该截图中 tag 值为 1 ,通过核对 JVM 常量池元素一览表 可知,tag 位标识为 1 对应的常量池元素为 CONSTANT_Utf8_info(含义:UTF-8编码字符串)。其组成结构分为3部分,分别是:tag、length、bytes,其中tag和length的长度分别是u1、u2(u后面的数字是几,就代表占几个字节),即分别占1字节和2字节。而 bytes 则是字符串的具体内容,其长度是 length 字节。在字节码文件中,该常量池元素最终所占的字节数是:1 + 2 + length 。其他类型的常量池元素的组成结构类似,这里不一一分析,下文在实例解析常量池组成结构时会在吃住个进行详细解析

3.4. 常量池第一个元素分析

常量池中的元素类型只可能是常量池元素结构中的那12种类型,而且每个元素结构的的第1个字节永远都是tag,可通过tag去对应常量池元素结构表中的数据结构,查看每个结构的含义。据此,我们来推断一下常量池中的第一个元素。

第一个常量池元素在魔数(4字节)、major version(2字节)、minor version(2字节)、常量池数量(2字节)之后,也就是从第11个字节开始,就是常量池中的的第一个常量池数组。

下图中第11位是 0x0A 转为十进制位为 10,参照常量池元素结构 图可知数据结构是CONSTANT_methodref_info。
在这里插入图片描述在这里插入图片描述
关于类型u1,u2,u4,u8的解释:
Class文件格式采用一种类似C语言结构伪结构存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型以u1,u2,u4,u8来分别代表一个字节、2个字节,4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字条串值。

由此可以推断出第一个元素占 1+2+2 = 5 个字节,进而绘制了如下表格

序号 类型 结构 对应16进制 转化为10进制 十进制含义
1 u1 tag 0x0A 10
2 u2 index 0x0009 9 指向常量池的第9个元素;
3 u2 index 0x001A 26 指向常量池的第26个元素;

有了上文的基础,我们就可以对常量池进行切割,方便我们定位元素的位置,常量池的数量是 0x0026 等于 38,真实的常量个数是 38-1 = 37个,之所以比字节码文件中的少一个,是因为JVM会保留地0号常量池位置,一次仅从第1个开始计算。见下图:

开始分析表格中的序号2:0x09 转化为十进制为9,指向常量池中的第9个元素,第9个元素是0x070022,tag为0x07的元素结构为
在这里插入图片描述
index 代表全限定类名的常量项索引,占用两个字节为 0x0022 ,转化为十进制为 34,由此可知,第九位元素又指向了常量池的第34位元素,第34位元素为0x01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74,正好每两个字节代表一个字符,对应的字符串是java/lang/Object,如下图所示:


序号3的原理一样,所以不再赘述。

分析完字节码的二进制后,我们再看一下通过命令 javap -verbose Test 获取 Test.class 对应的字节码。

不用多说,是不是有点意思。

3.5. 变量型常量池分析

查看类成员变量a的信息,见下图:
JVM字节码分析_第3张图片
上图中所选中的8个字节,一共包含两个常量池的元素信息,因为其tag位都是1,所以这两个常量池元素的类型都是字符串。
在这里插入图片描述
第一个字符串常量的length是1,对应的十六进制是0x61,正好对应UTF-8编码的字符a。第二个字符串常量的length也是1,对应的十六进制是0x49,正好对应UTF-8编码的字符I。在JVM规范中,若变量的类型是I,则表示该变量的实际类型是int。这与上文变量a的定义一致。

接着看类变量si的定义,如下图。
JVM字节码分析_第4张图片
上图中算选中的27个字节,一共描述了两个常量池元素,这两个常量池元素的类型也都是字符串。第一个字符串的length为2,其值是0x7369,对应的UTF-8编码的字符串si。第二个字符串的length为0x13,即19,其值是0x 4C 6A 61 76 61 2F 6C 61 6E 67 2F 49 6E 74 65 67 65 72 3E,这一串值是ASCII字符,每2个十六进制数对应一个ASCII字符,这些数字连接起来就对应一个字符串,所对应的字符是Ljava/lang/Integer。

接下来看类成员变量s的定义,如下图。
JVM字节码分析_第5张图片
上图中选中的25个字节,一共描述了两个常量池的元素,这两个常量池的元素类型也都是字符串。第一个字符串的lentgth为1,其值是0x73,对应UTF-8编码的字符s。第二个字符床的length为0x12,即18,其值是0x4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B,对应的UTF-8编码的字符串Ljava/lang/String。这两个常量池元素合起来描述了Test类中的s字符串变量。

4. 访问标识和继承信息

在上面我们讲了如何分析常量池的元素,但是一个.class 字节码文件中共有10个组成部分。下面将分析.class字节码文件的后续组成部分。

4.1. access_flags

在字节码文件中,常量池数组之后就是access_flags结构,该结构类型是u2,占两个字节。access_flags代表访问标志位,该标志用于标注类或者接口层次的访问信息,如当前Class是类还是接口,是否定义为public类型,是否定义为abstract类型等。从下图可以看到access_flags为0x0021。
在这里插入图片描述根据JVM的access_flags的可选值如下表所示。
JVM字节码分析_第6张图片
JVM字节码分析_第7张图片
因为access_flag = 0x0021 可以拆解成 0x0020 和 0x0001,由此可以知道该类的访问标识既包含ACC_PUBLIC,也包含ACC_SUPER。自JDK1.2以后,类被编译出来的invokespecial字节码指令是否允许使用的选项都是真的,因此access_flags的值都会带有ACC_SUPER标识位。

4.2. this_class

access_flags访问标识之后就是this_class结构,该结构的类型是u2,占两个字节。this_class 为0x0005记录当前类的全限定类名,其值指向常量池中对应的索引值。
JVM字节码分析_第8张图片

将十六进制0x0005转化为十进制为5,所以指向常量池中的第5个元素。上文我们已经分析过二进制流了,为了方便直观,我们直接看字节码,可以知道第五个元素是Test,因为没有建包,所以全限定类名就是Test。

JVM字节码分析_第9张图片

4.3. super_class

在this_class之后就是super_class结构,该结构的类型是u2,占2字节。super_class记录当前类的父类全限定类名,其值指向常量池中对应的索引值。将十六进制的0x0009转化为十进制为9,指向常量池的第9个元素。
JVM字节码分析_第10张图片

只要明白其原理,分析起来就特别的简单。由于Test.class并没有显式继承任何基类,因此编译时便默认继承java.lang.object。这与字节码中的super_class值是一致的。

4.4. interface
4.4.1 interfaces_count

super_class访问标识符之后就是interfaces_count结构,该结构类型是u2,占2字节。interface_count结构记录当前类所实现的接口数量。
JVM字节码分析_第11张图片
因为没有继承任何类,所以是0

4.4.2 interfaces

interfaces 表示接口索引集合,是一组u2类型数据集合,该结构描述当前类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合中。因为interfaces_count值为0,因此字节码文件中并没有intefaces信息。

5. 字段信息

5.1. fields_counts

访问标识符之后就fields_count,该结构类型是u2,占2字节,该值记录当前类中所定义的变量总数量,包括类成员变量和类变量(即静态变量)。
在这里插入图片描述
Test.class类一共包含3个变量,上图显示也是3个,正好相互验证。

5.2. fields_info fields

在字节码文件中,fields_count之后的是field结构,该结构长度不确定,不同类型所长长度是不同的。fields记录类中所定义的各个变量的详细信息,包括变量名,变量类型、访问标、属性等。

5.3. fields结构组成格式

要分析fields机构信息,首先需要清楚该结构的数据组成格式,其结构如下表所示
JVM字节码分析_第12张图片
说明:
access_flags,表示变量的访问标识,该值是可选的,由JVM规范规定。
name_index,表示变量的简单名称引用,占2字节,其值指向常量池的索引。
descriptor_index,表示变量的类型信息引用,占2字节,其值指向常量池的索引。
Fields结构实际上是一个数组,数组中的每一个元素的结构都如上表所示,即每一个元素都包含访问标识、米昂成索引、描述信息索引、属性数量和属性信息。其中,如果属性数量为0,则没有属性信息,由于访问标识、名称、描述信息、属性数量的字节长度是确定的,因此JVM可以在解析过程中计算出fields结构所占的全部字节数。
变量的access_flags,有下表所示的可选项。
JVM字节码分析_第13张图片
其中,ACC_PUBLIC、ACC_PRIVATE和ACC_PROTECTED这3个标识只能选择一个,结构中的字段必须有ACC_PUBLIC、ACC_STATIC和ACC_FINAL标志,class文件对此并无规定,这些都是java语言所要求的。

5.3.1. 变量a分析

在fields_count之后就是第一个fields,通过上文的fields组成结构表可知,fields一共有5个不部分,第一部分就是access_flags,,在二进制流中是0x0001,参考** access_flags可选项表**可知代表ACC_PUBLIC。第二部分是名称索引是0x000A转化为十进制是10,依此类推类型索引是0x000B为11
JVM字节码分析_第14张图片
通过字节码文件查看常量池中第10和第11位元素的内容
JVM字节码分析_第15张图片
因为在0x000B之后的2字节0x0000说明a的属性数量是0,因为没有属性,所以字段描述结构中的最后元素attributes也就不存在了。
这里的I是什么意思呢?在JVM规范中,每个变量/字段都有描述信息,描述信息主要描述字段的数据类型、方法的参数的参数列表(参数类型,参数数量,参数顺序)和返回值。根据描述规则,基本数据类型和代表无返回值的void类型都用一个大写的字符表示,而对象类型则用字符L加对象的全限定类名表示。为了压缩字节码文件的体积(字节码文件最终也占用服务器硬盘资源和内存资源),对于基本数据类型,JVM都仅使用一个大写字母标识。如下图所示是各个基本数据类型所对应的标识符。
JVM字节码分析_第16张图片
对于数组类型,每一维将使用一个前置的“[”字符俩藐视,如“int[]”将被记录为“[I,String[][]”将被记录为“[[Ljava/lang/String;”。
用于描述方法时,按照先参数列表,后返回值顺序描述,参数列表按照参数的严格顺序放在一组“()”之内,如方法“String getAll(int id, String name)” 的描述为“(I,Ljava.lang/String;)Ljava/lang/String;”
变量a总共占8个字节,如下图所示:
JVM字节码分析_第17张图片

5.3.2. 变量si和s

JVM字节码分析_第18张图片
JVM字节码分析_第19张图片
通过JVM规范,我们对si(棕色)和s(紫色)所在的二进制流分别标记了出来,因为其attrbutes_count都是0,所以都是占8个字节。变量si的access_flags是8,表示这是一个带有static修饰符的变量,而变量s的access_flags是0,表示该变量没有任何访问修饰符。
两个变量名称分别引用常量池中12和13号元素,对照上文的字节码文件可知,其变量类型分别是Ljava/lang/Integer和Ljava/lang/String。一次也可知,对于引用类型的变量字节码文件描述其变量类型的格式是“L+类全限定名”。

6. 方法信息

6.1 methods_count

在字节码文件中,紧跟在变量的结构fileds后面的是methods_count结构,该结构类型是u2,占2字节。该结构描述类中一共有多少个方法。
JVM字节码分析_第20张图片
由上图可知,其值是4,即Test类中一共有4个方法。可能很多人对此会有疑惑,在Test源程序中明明只有两个方法,为什么字节码文件中却像是了4个呢?这是因为在编译期间编译器会自动为一个类增加void()这样一个方法,其方法名就是“”,返回值为void。该方法的作用主要是执行类的初始化,源程序中所有static 类型的变量都在这个方法中完成初始化,全部被static{}所包围的程序都会在这个方法中执行。同时,在源代码中,并没有为Test类定义构造函数,因此编译器会自动为该类增加一个默认的构造函数。因此,字节码文件会显示Test类中一共包含4个方法。

6.2 methods_info methods

在methods_count后面的就是methods结构,这是一个数组,每一个方法的全部细节都包含在里面,包括代码指令。

6.2.1. methods结构组成格式

要分析methods结构信息,首先需要清楚该结构的数组组成格式,其格式如图4.7(方法表结构和字段结构一样)所示。
JVM字节码分析_第21张图片

由上表可知,方法各个数据项的含义非常相似,仅在访问标识位和属性表集合的可选项上有略微不同。这些字段的含义与上文给出的fields结构字段含义基本相同,因此这里不作具体说明。
其中,JVM规范为access_flags规定了一组可选项值,如下表所示

由于ACC_VOLATILE标志和ACC_TRANSIENT标志不能修饰方法,所以access_flags中不包含这两项,同时增加ACC_SYNCHRONIZED标志、ACC_STRICTFP标志和ACC_ABSTRACT标志。

6.2.2. 第一个方法void()

上面讲解了方法描述的信息结构,下面来实际看看Test.class字节码文件的第一个方法究竟是如何描述的。紧跟在methods_count后面的就是一个方法的信息,如下图所示。
JVM字节码分析_第22张图片

按照fields的机构组成格式,前2字节描述access_flags,即访问标识,其值为0x0001,对照上文所给出的方法访问标识可选项值的表可知,该值标识的方式的修饰符是public。
接下里的2字节藐视name_index,该字段描述的是方法名,其值指向常量池中对应的元素编号。由此上图可知。其值是0x0010,指向常量池中的第16号元素,参照根据字节码文件可知,常量池的第16号元素是,即当前所描述的方法名是“init”,这在上文提到过,该方法是java编译器在编译期间动态添加的类初始化的方法,而非在源程序中定义的。
当java类中定义了构造方法,或其他非static类成员变量被赋了初始值,编译器便会生成
接下来是descriptor_index,该字段描述的是方法的入参和出参,其指向常量池中对应的元素编号,有上图可知,其值是0x0011,指向常量池中第17号元素,根据字节码文件可知,常量池中的第17号元素是()V,这表示当前方法没有入参(因为是空括号),并且方法的返回类型是void(V代表viod)。这里要注意,按照JVM的规范,描述符对入参将严格按照源程序中所定义的参数列表顺序,从左到右依次放入“()”内,如方法“String getAll(int id, String name)”的描述符为“(I,Ljava/lang/String)Ljava/lang/String”。由此规律可以推断出,”()V”字节码描述的方法是void()。
接下来2字节描述的方法所包含的属性总数量attributes_count,由上图可知值为0x0001,表示当前方法一共包含1个属性。

6.2.3. 第二个方法void()

在这里插入图片描述
access_flags为 0x0008,表示AC_STATIC,这是一个static类型的静态方法。
name_index为 0x0017,该字段描述的方法名,指向常量池中的对应元素编号,为
descriptor_index为0x0011,该字段描述的是方法的入参和出参,其指向常量池中对应的元素编号,有上图可知,其值是0x0011,指向常量池中第17号元素,根据字节码文件可知,常量池中的第17号元素是()V,这表示当前方法没有入参(因为是空括号),并且方法的返回类型是void(V代表viod)。
attributes_count为0x0001,描述的方法所包含的属性总数量,可知,该方法有一个属性。

三、附件

https://download.csdn.net/download/qq_39774931/20333578

参考文献:《揭秘Java虚拟机-JVM设计原理与实现》

你可能感兴趣的:(JVM,jvm)