Java字节码方法表

引言

因为字段表和方法表的结构类似,所以我们直接分析Java字节码的方法表内容,理解了方法表,自然就理解了字段表

方法表

方法表在Class文件中的位置是在字段表之后的,具体的结构我们根据下表再来回顾一下

类型 名称 数量
u2 access_flas 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info 属性表 attributes_count

还是跟上篇文章一样,我们写一个简单的Java类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public class Test {

public String sayHello() {

String sayStr = "hello world";

return sayStr;

}

public static void main(String[] args) {

Test test = new Test();

System.out.println(test.sayHello());

}

}

使用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
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206

public class com.ymm.agent.Test

minor version: 0

major version: 52

flags: (0x0021) ACC_PUBLIC, ACC_SUPER

this_class: #3 // com/ymm/agent/Test

super_class: #8 // java/lang/Object

interfaces: 0, fields: 0, methods: 3, attributes: 1

Constant pool:

#1 = Methodref #8.#27 // java/lang/Object."<init>":()V

#2 = String #28 // hello world

#3 = Class #29 // com/ymm/agent/Test

#4 = Methodref #3.#27 // com/ymm/agent/Test."<init>":()V

#5 = Fieldref #30.#31 // java/lang/System.out:Ljava/io/PrintStream;

#6 = Methodref #3.#32 // com/ymm/agent/Test.sayHello:()Ljava/lang/String;

#7 = Methodref #33.#34 // java/io/PrintStream.println:(Ljava/lang/String;)V

#8 = Class #35 // java/lang/Object

#9 = Utf8 <init>

#10 = Utf8 ()V

#11 = Utf8 Code

#12 = Utf8 LineNumberTable

#13 = Utf8 LocalVariableTable

#14 = Utf8 this

#15 = Utf8 Lcom/ymm/agent/Test;

#16 = Utf8 sayHello

#17 = Utf8 ()Ljava/lang/String;

#18 = Utf8 sayStr

#19 = Utf8 Ljava/lang/String;

#20 = Utf8 main

#21 = Utf8 ([Ljava/lang/String;)V

#22 = Utf8 args

#23 = Utf8 [Ljava/lang/String;

#24 = Utf8 test

#25 = Utf8 SourceFile

#26 = Utf8 Test.java

#27 = NameAndType #9:#10 // "<init>":()V

#28 = Utf8 hello world

#29 = Utf8 com/ymm/agent/Test

#30 = Class #36 // java/lang/System

#31 = NameAndType #37:#38 // out:Ljava/io/PrintStream;

#32 = NameAndType #16:#17 // sayHello:()Ljava/lang/String;

#33 = Class #39 // java/io/PrintStream

#34 = NameAndType #40:#41 // println:(Ljava/lang/String;)V

#35 = Utf8 java/lang/Object

#36 = Utf8 java/lang/System

#37 = Utf8 out

#38 = Utf8 Ljava/io/PrintStream;

#39 = Utf8 java/io/PrintStream

#40 = Utf8 println

#41 = Utf8 (Ljava/lang/String;)V

{

public com.ymm.agent.Test();

descriptor: ()V

flags: (0x0001) ACC_PUBLIC

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: invokespecial #1 // Method java/lang/Object."<init>":()V

4: return

LineNumberTable:

line 9: 0

LocalVariableTable:

Start Length Slot Name Signature

0 5 0 this Lcom/ymm/agent/Test;

public java.lang.String sayHello();

descriptor: ()Ljava/lang/String;

flags: (0x0001) ACC_PUBLIC

Code:

stack=1, locals=2, args_size=1

0: ldc #2 // String hello world

2: astore_1

3: aload_1

4: areturn

LineNumberTable:

line 12: 0

line 13: 3

LocalVariableTable:

Start Length Slot Name Signature

0 5 0 this Lcom/ymm/agent/Test;

3 2 1 sayStr Ljava/lang/String;

public static void main(java.lang.String[]);

descriptor: ([Ljava/lang/String;)V

flags: (0x0009) ACC_PUBLIC, ACC_STATIC

Code:

stack=2, locals=2, args_size=1

0: new #3 // class com/ymm/agent/Test

3: dup

4: invokespecial #4 // Method "<init>":()V

7: astore_1

8: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;

11: aload_1

12: invokevirtual #6 // Method sayHello:()Ljava/lang/String;

15: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

18: return

LineNumberTable:

line 17: 0

line 18: 8

line 19: 18

LocalVariableTable:

Start Length Slot Name Signature

0 19 0 args [Ljava/lang/String;

8 11 1 test Lcom/ymm/agent/Test;

}

SourceFile: "Test.java"

通过上文的interfaces: 0, fields: 0, methods: 3, attributes: 1,我们可以知道该文件一共包含3个方法,分别对应着无参构造,sayHello,main三个方法

好了,现在让我们直接略过前面的一大推常量池项,

直接阅读我们自己写的sayHello方法的字节码内容

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

public java.lang.String sayHello();

descriptor: ()Ljava/lang/String; /* 方法描述符 */

flags: (0x0001) ACC_PUBLIC /* 访问标识 */

Code: /* code属性 也就是我们写的具体代码翻译成的字节码指令内容 */

stack=1, locals=2, args_size=1 /* 分别指操作数栈深度;本地变量所需的存储空间(slot为单位);参数个数 */

0: ldc #2 // String hello world /*将一个常量加载到操作数栈*/

2: astore_1 /* 将一个数值从操作数栈存储到局部变量表 */

3: aload_1 /* 将一个数值从局部变量表加载到操作数栈 */

4: areturn /* 将栈顶第一个元素返回 */

LineNumberTable: /* 字节码与java代码行数对应关系 一般用于调试 */

line 12: 0

line 13: 3

LocalVariableTable: /* 局部变量表 */

Start Length Slot Name Signature

0 5 0 this Lcom/ymm/agent/Test;

3 2 1 sayStr Ljava/lang/String;

根据上表我们知道方法表的第一个内容的是access_flas,占一个字节。表中的access_flas其实就对应着上文中的 flags: (0x0001) ACC_PUBLIC,标识这个方法是public的,接下在的name_indexdescriptor_index,在上文中的体现分别对应着sayHello()Ljava/lang/String;

这里解释一下描述符的概念,在介绍Class文件结构的文章中有提到,这里再提一遍:

描述符的作用是用来描述字段的数据类型,方法的参数列表和返回值,根据描述符规则,基本数据类型(byte,char,int,long,float,double,short,boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用L加对象的全限定名来表示

最重要的内容其实还是在attribute_info属性表中的code内容,也就是我们写的具体代码翻译成的字节码指令内容,这块内容是我们阅读字节码文件的重中之重。下面我们就来阅读一下上文的字节码内容

  1. ldc #2 将常量池中索引为2的字符串加载到操作数栈顶

  2. astore_1 将一个数值从操作数栈存储到局部变量表

  3. aload_1 将一个数值从局部变量表加载到操作数栈

  4. areturn 将栈顶第一个元素返回

可以看到上面的四个步骤的操作其实都是基于栈的操作,这里提一下java虚拟机栈的栈帧结构

image.png

关于字节码子令集可以参考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
25
26
27
28

public class Test {

public int inc() {

int x;

try {

x = 1;

return x;

} catch (Exception e) {

x = 2;

return x;

} finally {

x = 3;

}

}

}

代码非常简单,我想大家应该也都知道正确答案,当没有出现异常的时候,返回值为1,出现异常的话则为2(当然这里不会抛异常)。可是如果我们在finally快里加句代码System.out.prinln("do it"),然后再执行这个方法,其实是可以看到do it被打印了,也就是说在执行return之前,finally快中的代码是被执行了的,那么这里就有一个有趣的问题了,为何返回仍然是2而不是3呢?

下面我们就从字节码文件中来找出答案

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

public int inc();

descriptor: ()I

flags: (0x0001) ACC_PUBLIC

Code:

stack=1, locals=5, args_size=1

0: iconst_1 // 将int = 1的值压入栈顶

1: istore_1 // 弹出栈顶元素,存入位置为1的局部变量表

2: iload_1 // 从位置为1的局部变量中取出元素压入栈顶

3: istore_2 // 弹出栈顶元素,存入位置2的局部变量中

4: iconst_3 // 将int = 3的值压入栈顶 (这里执行finally块中的代码了)

5: istore_1 // 弹出栈顶元素,存入位置1的局部变量中

6: iload_2 // 从位置为2的局部变量中取出元素压入栈顶

7: ireturn // 返回栈顶元素2 (哈哈哈 看到没有,这里返回的是2,没有异常的话,这里方法就返回了)

8: astore_2 // 将栈顶的异常引用,存入位置2的局部变量中 (这里就是异常捕获的代码了)

9: iconst_2 // 将int = 2的值压入栈顶

10: istore_1 // 弹出栈顶元素,存入位置1的局部变量中

11: iload_1 // 从位置为1的局部变量中取出元素压入栈顶

12: istore_3 // 弹出栈顶元素,存入位置3的局部变量中

13: iconst_3 // 将int = 3的值压入栈顶

14: istore_1 // 弹出栈顶元素,存入位置1的局部变量中

15: iload_3 // 从位置为3的局部变量中取出元素压入栈顶

16: ireturn // 返回栈顶元素 2

17: astore 4 // 将栈顶异常引用存入位置为4的局部变量表中

19: iconst_3 // 将int = 3的值压入栈顶

20: istore_1 // 弹出栈顶元素,存入位置1的局部变量中

21: aload 4 将位置为4的局部变量引用压入栈顶

23: athrow // 将栈顶的异常抛出

Exception table: // 异常表

from to target type

0 4 8 Class java/lang/Exception

0 4 17 any

8 13 17 any

17 19 17 any

如果你已经看懂了上面的字节码,你应该会豁朗开朗。原因其实就是在于这里开辟了两个局部变量,finally块中的代码也确实执行了,只是将变量存入了局部变量表中的另一个位置,并且通过这个例子,也可以发现,无论什么情况finally块中的代码都会执行。

尾言

好了,本文到这里就差不多结束了。对于字节码的探索个人觉的还是非常有意思的,之后应该也会探索更多有意思的东西

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 王俊男的技术杂谈 All Rights Reserved.

访客数 : | 访问量 :