Java字节码初步学习
2024-08-09 18:24:04

背景引入

我们平常所写的 Java 是不能被 JVM 识别所运行的,必须将其编译成 class 文件,JVM 才能够识别代码,进行对应的程序操作。
Java 有着”一次编译,到处运行”的特点,也就是说当我们对一份 java 代码进行编译之后,得到的 class 文件能够放在任意机器或者任意 OS 系统中的 JVM 中识别并运行。当然 JVM 并不止单单支持 Java。

Java 字节码文件

class 文件本质是一个以 8 位字节为基础的二进制流。所有的 class 文件开头的前四个字节都是固定的魔数– 0xCAFEBABE (咖啡宝贝)。当然这个主要是用来让 JVM 识别该文件是否为 class 文件
该 class 文件编译之前采用的 Java 版本在魔数之后的 4 个字节给出,前两个字节表示次版本号,后两个字节表示主版本号。class 文件的 10 进制形式能够在 oracle 官网进行版本查询,得知更加具体的版本号
比如我此时编译一份 java 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.example;

public class person {
private int num = 1;
public static int NUM = 100;

public int func(int a, int b) {
return add(a, b);
}

public int add(int a, int b) {
return a + b + num;
}

public int sub(int a, int b) {
return a - b - NUM;
}
}

image.png
这还只是 class 文件的二进制形式,我们能够通过其他工具查看到更加具体的 class 字节码内容。
比如 idea 的自带的 view 功能。这里我们依然选择 person 类编译之后的 class 文件进行查看
选定 person.class 文件之后点击上方的 view,然后选择 Show ByteCode
image.png
就可以得到这么一份还算比较好看的 Bytecode
image.png
或者我们采用javapjava 原生的字节码工具也能够查看。
来一份 javap 的参数表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-help  --help  -?        输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置

这里用 javap -verbose -p person.class 生成的字节码查看如下

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
public class org.example.person
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#27 // org/example/person.num:I
#3 = Methodref #5.#28 // org/example/person.add:(II)I
#4 = Fieldref #5.#29 // org/example/person.NUM:I
#5 = Class #30 // org/example/person
#6 = Class #31 // java/lang/Object
#7 = Utf8 num
#8 = Utf8 I
#9 = Utf8 NUM
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lorg/example/person;
#17 = Utf8 func
#18 = Utf8 (II)I
#19 = Utf8 a
#20 = Utf8 b
#21 = Utf8 add
#22 = Utf8 sub
#23 = Utf8 <clinit>
#24 = Utf8 SourceFile
#25 = Utf8 person.java
#26 = NameAndType #10:#11 // "<init>":()V
#27 = NameAndType #7:#8 // num:I
#28 = NameAndType #21:#18 // add:(II)I
#29 = NameAndType #9:#8 // NUM:I
#30 = Utf8 org/example/person
#31 = Utf8 java/lang/Object
{
private int num;
descriptor: I
flags: ACC_PRIVATE

public static int NUM;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC

public org.example.person();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field num:I
9: return
LineNumberTable:
line 3: 0
line 4: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lorg/example/person;

public int func(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: aload_0
1: iload_1
2: iload_2
3: invokevirtual #3 // Method add:(II)I
6: ireturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lorg/example/person;
0 7 1 a I
0 7 2 b I

public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: aload_0
4: getfield #2 // Field num:I
7: iadd
8: ireturn
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lorg/example/person;
0 9 1 a I
0 9 2 b I

public int sub(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: isub
3: getstatic #4 // Field NUM:I
6: isub
7: ireturn
LineNumberTable:
line 16: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lorg/example/person;
0 8 1 a I
0 8 2 b I

static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 100
2: putstatic #4 // Field NUM:I
5: return
LineNumberTable:
line 5: 0
}

发现结果还是不同的,javap 的生成感觉可读性更高。两者内容比较是没有问题的。javap -verbose -p person.class的内容直接把常量池中的所有信息都打印出来,我们也能够更好的查找。

常量池(Constant pool)

最显著的就是我们最开始用javap -verbose -p person.class产生的结果中那一堆用#表示的信息。
常量池可以理解为 class 文件中的资源仓库,主要存放两大类常量:字面量(Literal),和符号引用(Symbolic References) 。字面量就类似于 java 中的常量概念,如文本字符串,final 常量等。而符号引用则属于编译原理方面的概念,包含以下三种:

  • 类接口的全限定名
  • 字段的名称和描述符号
  • 方法的名称和描述

我们截取生成字节码中常量池的部分内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Constant pool:
#1 = Methodref #6.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#27 // org/example/person.num:I
#3 = Methodref #5.#28 // org/example/person.add:(II)I
#4 = Fieldref #5.#29 // org/example/person.NUM:I
#5 = Class #30 // org/example/person
#6 = Class #31 // java/lang/Object
#7 = Utf8 num
#8 = Utf8 I
#10 = Utf8 <init>
#11 = Utf8 ()V
#26 = NameAndType #10:#11 // "<init>":()V
#27 = NameAndType #7:#8
#31 = Utf8 java/lang/Object

举个例子,来看#1 = Methodref #6.#26 // java/lang/Object."<init>":()V
它是一个方法引用(Methodref),指向了第 6 个和第 26 个常量,以此类推看第六个和第 26 个常量的组成,我们能够拼接出后面//注释掉的内容java/lang/Object."<init>":()V
这段其实可以理解为改类的实例构造器说明,由于我们所写的 person 方法没有重写构造方法,所以会调用到父类 Object 的构造方法,该方法的返回值为 V,也就是 void 为空
再来第二个常量举例 #2 = Fieldref #5.#27 // org/example/person.num:I
表明他是一个属性引用,由#5 和#27 常量组成,还是类推过去,我们可以得到后续注释一模一样的内容org/example/person.num:I这就是表明该常量#2 代表org/example/包下 person 类的属性 num,并且类型是 Int
第三个稍微记录一下org/example/person.add:(II)I 表明该方法引用代表org/example包下的 person 类的 add 方法,该方法由两个参数,并且返回值为 Int
这些都是与我们所写的 java 代码中能对应上的,只不过好像初始值没给出,还只是对其变量的一个 ref
最后类型的定义,稍微记录一下
image.png
对于数组类型,每一位使用一个前置的[字符来描述,如定义一个java.lang.String[][]类型的维数组,将被记录为[[Ljava/lang/String;
补充一组访问标识:也就是各块描述中 flag=????的内容
image.png

方法表集合

最开始常量池的定义中我们单单只说它是字面量(Literal),和符号引用(Symbolic References)的资源仓库,并不表示类方法的具体内容,而紧接常量池的内容下面就是方法表集合。在字节码中,方法以表的集合形式表现。由于各种工具编码的问题,其实该处方法表的内容每个工具都有可能产生不同的细节,我们拿当前 javap 生成的字节码来分析类比
这里就拿一个构造方法来看

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
  public org.example.person();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field num:I
9: return
LineNumberTable:
line 3: 0
line 4: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lorg/example/person;

//上面是javap的内容,下面是idea的内容

public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 4 L1
ALOAD 0
ICONST_1
PUTFIELD org/example/person.num : I
RETURN
L2
LOCALVARIABLE this Lorg/example/person; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1

具体分析:

  • descriptor: ()Vdescriptor 主要是对该方法返回值的一个描述
  • flags: ACC_PUBLIC表示该方法是公共的,具体还有一些 flag 标识参考上方访问标识。

code 块具体解析:

  • stack=2最大操作数栈,JVM 运行时会根据该值来分配栈帧中的操作栈深度,此处为 1(懵,学少了)
  • locals局部变量所需的存储空间,单位为 slot,slot 是虚拟机为局部变量分配内存时所使用的最小单位,为 4 个字节大小。其中,方法参数(实例方法中会固定隐藏一个 this),显示异常处理器参数(trycatch 块中 catch 所定义的异常),方法体中定义的局部变量,都需要使用局部变量表来存放。 locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的 。(…懵)
  • args_size方法参数的个数,这里实例化方法为什么会有 1 个参数呢?每个实例方法都会隐藏一个 this 参数
  • attribute_info这一块呢就是左边是 数字: 操作栈帧的所有内容了,叫做方法体内容。具体分析这些操作指令

aload_0:如果是aload_x,则表示从局部变量表中相应位置 x 装载一个对象引用到操作数栈的栈顶。如果是 aload_0,则表示把第零个引用类型本地变量(this 指针)推送到操作数栈顶。
invokespecial #1:弹栈,并执行 #1 方法。这里我们首先是进行了一波aload_0,也就是把 this 变量压入了栈,然后 invoke 弹栈,将 this 弹出,并作为参数调用进了初始化方法。
iconst_1:将 int 型常量 1 推送至栈顶(留个伏笔,我们 person 不是有两个定义的属性吗)
putfield:其实按英文翻译都能猜出来是在干嘛,其实就是为属性赋值。接受一个操作数,这个操作数引用的是运行时常量池的一个字段,比如我们这里#2,那么对应常量池就是 #2 = Fieldref #5.#27 // org/example/person.num:I,为 person 类的 num 属性值。putfield 会弹出两个栈顶两个值,我们刚才栈顶的值为 this 传入了构造 init 方法,所以 putfield 操作之后执行的就是 this.num=1
最后一个操作码return,也就是返回空内容,方法执行结束。
还有一些 invoke 操作栈帧没有提及:

1
2
3
4
5
invokestatic:调用静态方法
invokespecial:调用实例构造方法 调用私有方法 调用父类方法
invokeinterface:调用接口方法
invokevirtual:调用虚方法(除上面三种情况之外的方法,如调用对象方法)
invokedynamic:Lambda的原理,即动态调用

当然还有很多指令栈帧没有提及,我们之后分析的时候遇到了再记录。