前言

在上一章节 Java-JVM类文件结构 中描述了Class文件的组成,为了加深影响,这章将进行手动实践,编写一个Java示例文件,对编译生成后的Class文件进行一个一个字节的分析。

Java文件

以下是示例文件的Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.itwray.study.advance.jvm;

/**
* 字节码分析类
*
* @author wray
* @since 2024/7/25
*/
public class Main {

private int num;

private static final String name = "wray";

public static void main(String[] args) {
Main main = new Main();
main.num++;
main.print(name + main.num);
}

private void print(String arg) {
System.out.println("Hello " + arg);
}
}

Class文件

通过 javac Main.java 命令生成Class文件,文件内容如下(本文使用Sublime Text):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
cafe babe 0000 0034 003e 0a00 0f00 2507
0026 0a00 0200 2509 0002 0027 0700 280a
0005 0025 0800 290a 0005 002a 0a00 0500
2b0a 0005 002c 0a00 0200 2d09 002e 002f
0800 300a 0031 0032 0700 3301 0003 6e75
6d01 0001 4901 0004 6e61 6d65 0100 124c
6a61 7661 2f6c 616e 672f 5374 7269 6e67
3b01 000d 436f 6e73 7461 6e74 5661 6c75
6501 0006 3c69 6e69 743e 0100 0328 2956
0100 0443 6f64 6501 000f 4c69 6e65 4e75
6d62 6572 5461 626c 6501 0012 4c6f 6361
6c56 6172 6961 626c 6554 6162 6c65 0100
0474 6869 7301 0023 4c63 6f6d 2f69 7477
7261 792f 7374 7564 792f 6164 7661 6e63
652f 6a76 6d2f 4d61 696e 3b01 0004 6d61
696e 0100 1628 5b4c 6a61 7661 2f6c 616e
672f 5374 7269 6e67 3b29 5601 0004 6172
6773 0100 135b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 0100 0570 7269 6e74
0100 1528 4c6a 6176 612f 6c61 6e67 2f53
7472 696e 673b 2956 0100 0361 7267 0100
0a53 6f75 7263 6546 696c 6501 0009 4d61
696e 2e6a 6176 610c 0015 0016 0100 2163
6f6d 2f69 7477 7261 792f 7374 7564 792f
6164 7661 6e63 652f 6a76 6d2f 4d61 696e
0c00 1000 1101 0017 6a61 7661 2f6c 616e
672f 5374 7269 6e67 4275 696c 6465 7201
0004 7772 6179 0c00 3400 350c 0034 0036
0c00 3700 380c 0020 0021 0700 390c 003a
003b 0100 0648 656c 6c6f 2007 003c 0c00
3d00 2101 0010 6a61 7661 2f6c 616e 672f
4f62 6a65 6374 0100 0661 7070 656e 6401
002d 284c 6a61 7661 2f6c 616e 672f 5374
7269 6e67 3b29 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 6742 7569 6c64 6572 3b01
001c 2849 294c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 4275 696c 6465 723b 0100
0874 6f53 7472 696e 6701 0014 2829 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
0100 106a 6176 612f 6c61 6e67 2f53 7973
7465 6d01 0003 6f75 7401 0015 4c6a 6176
612f 696f 2f50 7269 6e74 5374 7265 616d
3b01 0013 6a61 7661 2f69 6f2f 5072 696e
7453 7472 6561 6d01 0007 7072 696e 746c
6e00 2100 0200 0f00 0000 0200 0200 1000
1100 0000 1a00 1200 1300 0100 1400 0000
0200 0700 0300 0100 1500 1600 0100 1700
0000 2f00 0100 0100 0000 052a b700 01b1
0000 0002 0018 0000 0006 0001 0000 0009
0019 0000 000c 0001 0000 0005 001a 001b
0000 0009 001c 001d 0001 0017 0000 006d
0003 0002 0000 002d bb00 0259 b700 034c
2b59 b400 0404 60b5 0004 2bbb 0005 59b7
0006 1207 b600 082b b400 04b6 0009 b600
0ab7 000b b100 0000 0200 1800 0000 1200
0400 0000 1000 0800 1100 1200 1200 2c00
1300 1900 0000 1600 0200 0000 2d00 1e00
1f00 0000 0800 2500 1c00 1b00 0100 0200
2000 2100 0100 1700 0000 5200 0300 0200
0000 1ab2 000c bb00 0559 b700 0612 0db6
0008 2bb6 0008 b600 0ab6 000e b100 0000
0200 1800 0000 0a00 0200 0000 1600 1900
1700 1900 0000 1600 0200 0000 1a00 1a00
1b00 0000 0000 1a00 2200 1300 0100 0100
2300 0000 0200 24

魔数

1
u4             magic; //Class 文件的标志

从魔数开始,魔数是u4的无符号数,对应字节为 cafe babe

版本号

u2             minor_version;//Class 的小版本号
u2             major_version;//Class 的大版本号

Class的小版本号是u2的无符号数,对应字节为 0000 ,表示小版本号为0。

Class的大版本号是u2的无符号数,对应字节为 0034 ,表示大版本号为52,即Java 8版本。

常量池

u2             constant_pool_count;//常量池的数量
cp_info        constant_pool[constant_pool_count-1];//常量池

常量池的数量是u2的无符号数,对应字节为 003e ,对应十进制为62,表示常量池中有61个常量。

因此,在003e后面的cp_info对应有61个常量,按照常量特性,第一个u1无符号数表示标志位,用于确定常量类型,接下来一个一个的列举出来。

  1. 0a对应十进制为10,代表 CONSTANT_Methodref_info ,表示类中方法的符号引用,它对应的结构定义为如下:

    image-20240730135800638

    那么后面对应的值就是000f0025,十进制为15和37,分别表示方法返回类型的Class常量索引为15,方法的名称和描述符的常量索引为37。

    1
    #1 = Methodref          #15.#37        // java/lang/Object."<init>":()V
  2. 07对应十进制为7,代表 CONSTANT_Class_info ,表示类或接口的符号引用,它对应的结构定义如下:

    image-20240730141438882

    后面对应的值就是0026,十进制为38,表示这个类的全限定名的常量索引为38。

    1
    #2 = Class              #38            // com/itwray/study/advance/jvm/Main

    可以发现,每个常量的名称,无论是全限定名还是简单名称,到最后都会指向 CONSTANT_Utf8_info 常量,表示字符串的意思。

  3. 0a同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为00020025,十进制为2和37,分别表示方法返回类型的Class常量索引为2,方法的名称和描述符的常量索引为37。

    1
    #3 = Methodref          #2.#37         // com/itwray/study/advance/jvm/Main."<init>":()V

    通过第一个常量和第三个常量,可以发现它们的名称和描述符指向了同一个常量,说明Class文件中允许多个不同的方法有相同的名称和描述符,只要返回值不同,也是可以在一个Class文件中共存,这点与Java代码的重载(Overload)有点不同。

  4. 09对应十进制为9,代表 CONSTANT_Fieldref_info ,表示字段的符号引用,它对应的结构定义如下:

    image-20240730143401427

    那么后面对应的值就是00020027,十进制为2和39,分别表示字段所在的Class常量索引为2,字段的名称和描述符的常量索引为39。

    1
    #4 = Fieldref           #2.#39         // com/itwray/study/advance/jvm/Main.num:I
  5. 07同第2个常量的类型一样,代表 CONSTANT_Class_info ,对应的值是0028,十进制为40,表示这个类的全限定名的常量索引为40。

    1
    #5 = Class              #40            // java/lang/StringBuilder
  6. 0a同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为00050025,十进制为5和37,分别表示方法返回类型的Class常量索引为5,方法的名称和描述符的常量索引为37。

    1
    #6 = Methodref          #5.#37         // java/lang/StringBuilder."<init>":()V
  7. 08对应十进制为8,代表 CONSTANT_String_info ,表示字符串类型字面量,它的结构定义如下:

    image-20240730160946175

    对应的值就是0029,十进制为41,表示这个字符串对应的 CONSTANT_Utf8_info 索引为41。

    1
    #7 = String             #41            // wray
  8. 0a同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为0005002a,十进制为5和42,分别表示方法返回类型的Class常量索引为5,方法的名称和描述符的常量索引为42。

    1
    #8 = Methodref          #5.#42         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  9. 0a同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为0005002b,十进制为5和43,分别表示方法返回类型的Class常量索引为5,方法的名称和描述符的常量索引为43。

    1
    #9 = Methodref          #5.#43         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  10. 0a同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为0005002c,十进制为5和44,分别表示方法返回类型的Class常量索引为5,方法的名称和描述符的常量索引为44。

1
#10 = Methodref          #5.#44         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  1. 0a同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为0002002d,十进制为2和45,分别表示方法返回类型的Class常量索引为2,方法的名称和描述符的常量索引为45。

    1
    #11 = Methodref          #2.#45         // com/itwray/study/advance/jvm/Main.print:(Ljava/lang/String;)V
  2. 09同第4个常量的类型一样,代表 CONSTANT_Fieldref_info ,对应为002e002f,十进制为46和47,分别表示字段所在的Class常量索引为46,字段的名称和描述符的常量索引为47。

    1
    #12 = Fieldref           #46.#47        // java/lang/System.out:Ljava/io/PrintStream;
  3. 08同第7个常量的类型一样,代表 CONSTANT_String_info ,对应值为0030,十进制为48,表示这个字符串对应的 CONSTANT_Utf8_info 索引为48。

    1
    #13 = String             #48            // Hello
  4. 0a同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为00310032,十进制为49和50,分别表示方法返回类型的Class常量索引为49,方法的名称和描述符的常量索引为50。

    1
    #14 = Methodref          #49.#50        // java/io/PrintStream.println:(Ljava/lang/String;)V
  5. 07同第2个常量的类型一样,代表 CONSTANT_Class_info ,对应的值是0033,十进制为51,表示这个类的全限定名的常量索引为51。

    1
    #15 = Class              #51            // java/lang/Object
  6. 01对应十进制为1,代表 CONSTANT_Utf8_info,表示UTF-8编码的字符串,它的结构定义如下:

    image-20240730163314160

    对应的length选项值为0003,对应十进制为3,说明bytes选项的无符号数长度为3,即6e756d,对应的字符串为num

    1
    #16 = Utf8               num
  7. 从第17个常量到第36个常量,以及索引为38、40、41、48、51~61的常量,同第16个常量的类型一样,代表 CONSTANT_Utf8_info,它们对应的字符串如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #17 = Utf8               I
    #18 = Utf8 name
    #19 = Utf8 Ljava/lang/String;
    #20 = Utf8 ConstantValue
    #21 = Utf8 <init>
    #22 = Utf8 ()V
    #23 = Utf8 Code
    #24 = Utf8 LineNumberTable
    #25 = Utf8 LocalVariableTable
    #26 = Utf8 this
    #27 = Utf8 Lcom/itwray/study/advance/jvm/Main;
    #28 = Utf8 main
    #29 = Utf8 ([Ljava/lang/String;)V
    #30 = Utf8 args
    #31 = Utf8 [Ljava/lang/String;
    #32 = Utf8 print
    #33 = Utf8 (Ljava/lang/String;)V
    #34 = Utf8 arg
    #35 = Utf8 SourceFile
    #36 = Utf8 Main.java
    #38 = Utf8 com/itwray/study/advance/jvm/Main
    #40 = Utf8 java/lang/StringBuilder
    #41 = Utf8 wray
    #48 = Utf8 Hello
    #51 = Utf8 java/lang/Object
    #52 = Utf8 append
    #53 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
    #54 = Utf8 (I)Ljava/lang/StringBuilder;
    #55 = Utf8 toString
    #56 = Utf8 ()Ljava/lang/String;
    #57 = Utf8 java/lang/System
    #58 = Utf8 out
    #59 = Utf8 Ljava/io/PrintStream;
    #60 = Utf8 java/io/PrintStream
    #61 = Utf8 println
  8. 第37个常量,对应的字节码位置如下:

    image-20240730172445090

    0c的十进制为12,代表 CONSTANT_NameAndType_info ,表示字段或方法的部分符号引用,它的结构定义如下: image-20240730172707842

    对应的值是00150016,十进制为21和22,分别表示对应的 CONSTANT_Utf8_info 常量的索引为21和22。

    1
    #37 = NameAndType        #21:#22        // "<init>":()V

    关于如何在字节码文件中快速定位数据所处的字节码位置:在已知该项数据的上一个数据的值的情况下,可以根据数据的类型和值 反算出16进制编码,然后在编辑器中Ctrl + F搜索即可。(最好结合javap -verbose Xxx命令判断,避免找错)

  9. 常量索引为39、42、43、44、45、47、50的常量,均同第37个常量的类型一样,代表 CONSTANT_NameAndType_info ,它们对应的数据如下:

    1
    2
    3
    4
    5
    6
    7
    #39 = NameAndType        #16:#17        // num:I
    #42 = NameAndType #52:#53 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    #43 = NameAndType #52:#54 // append:(I)Ljava/lang/StringBuilder;
    #44 = NameAndType #55:#56 // toString:()Ljava/lang/String;
    #45 = NameAndType #32:#33 // print:(Ljava/lang/String;)V
    #47 = NameAndType #58:#59 // out:Ljava/io/PrintStream;
    #50 = NameAndType #61:#33 // println:(Ljava/lang/String;)V
  10. 第46个常量同第2个常量的类型一样,代表 CONSTANT_Class_info ,其对应的16进制字节码位置可以按照前面的搜索逻辑查询,16进制结果为39,十进制为57,表示这个类的全限定名的常量索引为57。

    1
    #46 = Class              #57            // java/lang/System
  11. 第49个常量同第2个常量的类型一样,代表 CONSTANT_Class_info ,16进制结果为3c,十进制为60,表示这个类的全限定名的常量索引为60。

    1
    #49 = Class              #60            // java/io/PrintStream

至此,61个常量就分析完了。确定常量池最后在字节码文件中结束的位置如下:

image-20240730174718030

01 0007 7072 696e 746c6e是最后一个 CONSTANT_Utf8_info 常量的字节码内容,对应的字符串为println

访问标志

1
u2             access_flags;//Class 的访问标记

访问标志是一个u2无符号数,对应的字节码是0021,通过标志位表查询的结果为:ACC_PUBLIC、ACC_SUPER。

类索引、父类索引、接口索引集合

u2             this_class;//类索引
u2             super_class;//父类索引
u2             interfaces_count;//接口数量
u2             interfaces[interfaces_count];//一个类可以实现多个接口

类索引是u2无符号数,对应字节码为0002,十进制为2,表示当前类文件的类对应为常量池中索引为2的常量。

1
#2 = Class              #38            // com/itwray/study/advance/jvm/Main

父类索引是u2无符号数,对应字节码为000f,十进制为15,表示当前类文件的父类对应为常量池中索引为15的常量。

1
#15 = Class              #51            // java/lang/Object

接口索引集合的结构如下:

1
2
u2             interfaces_count;//接口数量
u2 interfaces[interfaces_count];//一个类可以实现多个接口

对应字节码为0000,十进制为0,表示当前类文件没有接口。

字段表集合

u2             fields_count;//字段数量
field_info     fields[fields_count];//一个类可以有多个字段

字段表是先以一个u2无符号数表示字段数量,对应字节码为0002,十进制为2,表示类中有2个字段。

字段的结构定义固定如下:

image-20240725171340663

access_flags的标志字典如下:

image-20240725172852283

接下来一个个字段的分析:

  1. access_flags对应字节码为0002,表示ACC_PRIVATE;

    name_index对应字节码为0010,十进制为16,表示字段的简单名称对应常量池的索引为16,即\#16 = Utf8 num

    descriptor_index对应字节码为0011,十进制为17,表示字段的描述符对应常量池的索引为17,即\#17 = Utf8 I

    attributes_count对应字节码为0000,说明该字段没有属性信息,即没有attributes_info。

    通过匹配常量池的索引,该字段为:private int num

  2. access_flags对应字节码为001a0010表示ACC_FINAL,000a则是00020008的按位或运算结果,所以还表示ACC_PRIVATE、ACC_STATIC。

    name_index对应字节码为0012,十进制为18,表示字段的简单名称对应常量池的索引为18,即\#18 = Utf8 name

    descriptor_index对应字节码为0013,十进制为19,表示字段的描述符对应常量池的索引为19,即\#19 = Utf8 Ljava/lang/String;

    attributes_count对应字节码为0001,说明该字段有1个属性信息,根据属性表的结构定义,开始是一个u2无符号数,表示属性名称在常量池中的索引,对应字节码为0014,十进制为20,对应常量池的\#20 = Utf8 ConstantValueConstantValue的属性结构如下:

    image-20240731151436047

    根据属性结构得出紧接着后面是一个u4无符号数,表述属性的长度,对应字节码0000 0002,表示该属性的长度是2个u1(与属性结构中的第三个u2刚好对应上)。对应的属性长度的字节码为0007,通过ConstantValue标志表示这个字段是一个常量属性,然后通过属性长度的字段找到常量池中索引为7的常量,内容为\#7 = String #41 // wray,说明这个常量字段的值是一个字符串,字符串内容对应常量池中索引41,即\#41 = Utf8 wray

    最后通过匹配常量池的索引,该字段为:private static final String name = "wray"

方法表集合

1
2
u2             methods_count;//方法数量
method_info methods[methods_count];//一个类可以有多个方法

方法表同字段表的结构几乎一样,先是以一个u2无符号数表示方法数量,对应字节码为0003,表示当前类有3个方法。方法表的结构定义与字段表一样:

image-20240729160002172

只是access_flags访问标志有一点区别,访问标志字典如下:

image-20240729160514905

接下来一个个方法的分析:

  1. access_flags对应字节码为0001,表示ACC_PUBLIC;

    name_index对应字节码为0015,十进制为21,表示字段的简单名称对应常量池的索引为21,即\#21 = Utf8 <init>

    descriptor_index对应字节码为0016,十进制为22,表示字段的描述符对应常量池的索引为22,即\#22 = Utf8 ()V

    从上面三个标志可知,这是当前类无参构造函数。

    attributes_count对应字节码为0001,说明该字段有1个属性信息,根据属性表的结构定义,开始是一个u2无符号数,表示属性名称在常量池中的索引,对应字节码为0017,十进制为23,对应常量池的\#23 = Utf8 CodeCode属性的结构定义如下:

    image-20240731151714048

    Code属性较为复杂,接下来一个个类型再分析:

    1. attribute_name_index 在Code属性中肯定对应的是常量池中的Code常量,也就是属性最开始的u2无符号数,即0017
    2. attribute_lenth表示属性值的长度,由于属性名称索引与属性长度一共为6个字节,所以属性值的长度固定为整个属性表长度减去6个字节。对应字节码为0000 002f,十进制为47,所以属性值长度为41。
    3. max_stack代表操作数栈深度的最大值,
    4. 在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。对应字节码为0001
    5. max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。对应字节码为0001

    对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放。

    注意,并不是在方法中用了多少个局部变量,就把这些局部变量所占变量槽数量之和作为max_locals的值,操作数栈和局部变量表直接决定一个该方法的栈帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费。Java虚拟机的做法是将局部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出max_locals的大小。

    1. code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。

      code_lenth对应字节码为0000 0005,说明code有5个u1,那么code对应字节码为2ab7 0001 b1。每一个u1对应一个字节码指令,具体指令可以参考《深入理解Java虚拟机》附录C“虚拟机字节码指令表”。

    2. exception_table_length和exception_table表示方法的异常表,异常表有自己的表结构定义(从上到下、从左到右): image-20240731154308343

      exception_table_length对应字节码为0000,表示没有异常表,所以exception_table为空。

    3. attributes_count和attributes则表示属性表(attribute_info),说明Code属性内部可以包含其他属性,例如LineNumberTableLocalVariableTable等子属性。attributes_count对应字节码为0002,表示有两个属性表,属性表结构如下:

      image-20240731180451803

      接下来一个个属性分析:

      1. attribute_name_index对应字节码为0018,十进制为24,说明该属性的类型在常量池索引为24中,即 \#24 = Utf8 LineNumberTableLineNumberTable属性的结构这里就不在过多分析了。

        !!!只需要记住,在属性表结构中通过attribute_lenth确定了长度,attribute_length个u1就是这个属性的总长度,无论这个属性的内部结构怎么变,最后的总长度就是 u2 + u4 + attribute_length个u1。

        再看attribute_lenth对应字节码为0000 0006,表示info的长度为6,对应字节码为0001 0000 0009

      2. attribute_name_index对应字节码为0019,十进制为25,说明该属性的类型在常量池索引为25中,即\#25 = Utf8 LocalVariableTable。attribute_lenth对应字节码为0000 000c,表示info的长度为12,对应字节码为0001 0000 0005 001a 001b 0000

  2. 接下来是第二个方法,access_flags对应字节码为0009,对应的字节码标志值为00010008,即表示ACC_PUBLIC、ACC_STATIC;

    name_index对应字节码为001c,十进制为28,表示字段的简单名称对应常量池的索引为28,即\#28 = Utf8 main

    descriptor_index对应字节码为001d,十进制为29,表示字段的描述符对应常量池的索引为29,即\#29 = Utf8 ([Ljava/lang/String;)V

    从上面三个标志可知,该方法的定义为:public static void main(String[] arg0)

    其中arg0参数名称是不被虚拟机关注的,可以通过方法对应的属性表找到LocalVariableTable属性以确定实际代码中的参数名称。

    attributes_count对应字节码为0001,说明该字段有1个属性信息,根据属性表的结构定义,开始是一个u2无符号数,表示属性名称在常量池中的索引,对应字节码为0017,十进制为23,对应常量池的\#23 = Utf8 Code。因为与第一个方法属性一样,就不再展开分析了,按照属性表的通用结构定义直接分析字节码,再次展示一遍属性表的结构定义:

    image-20240731180451803

    attribute_length对应字节码为0000 006d,十进制为109,说明info有109个u1无符号数,对应字节码如下(灰色选中区域):

    image-20240801103318748

  3. 第三个方法,access_flags对应字节码为0002,表示ACC_PRIVATE;

    name_index对应字节码为0020,十进制为32,表示字段的简单名称对应常量池的索引为32,即\#32 = Utf8 print

    descriptor_index对应字节码为0021,十进制为33,表示字段的描述符对应常量池的索引为33,即\#33 = Utf8 (Ljava/lang/String;)V

    从上面三个标志可知,该方法的定义为:private void print(String arg0)

    attributes_count对应字节码为0001,说明该字段有1个属性信息,根据属性表的结构定义,开始是一个u2无符号数,表示属性名称在常量池中的索引,对应字节码为0017,十进制为23,对应常量池的\#23 = Utf8 Code。同前两个方法一样,直接看attribute_length对应字节码为00 0000 52,十进制为82,info对应的82个字节码如下(灰色选中区域):

    image-20240801104128103

属性表集合

1
2
u2             attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合

首先第一个u2无符号数表示属性表的属性数,对应字节码为0001,表示有一个属性。

同分析方法表中的属性一样,再次根据attribute_info(属性表)的结构定义分析:

image-20240731180451803

attribute_name_index对应字节码为0023,十进制为35,对应常量池中索引为35的常量,即\#35 = Utf8 SourceFile

接下来是attribute_length,对应字节码为00 0000 02,表示info的长度为2。

info对应的字节码为00 24

至此,这个Class文件分析完毕(完结撒花~)。

总结

从魔数到最后的属性表集合,一个个u1无符号数分析下来,不得不感慨Class文件结构的紧凑,因为它真的没有任何一个分隔符,但即使文件结构紧凑,仍然提供了很多可扩展的特性,并且通过《Java虚拟机规范》实现了平台无关性、语言无关性。

而且,这还是从Java初版到至今,仍然维持Class文件结构几乎不变,且功能稳定实用。

再次佩服Java语言设计者们的智慧。