3.2.1javap命令工具
第1章中我们就提到了有些地方需要用javap命令工具来看编译后的指令是什么,第2.2.1节中胖哥使用了一个简单的程序让大家感受了一下javap命令工具是什么,这里再次谈到javap命令工具了。或许这一次我们可以对javap命令工具说得稍微清楚一点。为此,胖哥会单独再写几段小程序给大家说说javap命令工具的结果怎么看。
1
2
3胖哥为什么要给简单程序呢?为啥不直接来个复杂的程序呢? 答曰:javap命令工具输出的内容是繁杂的,即使是一段小程序输出后,结果也比原始代码要复杂很多。我们要学的其实并不是说看指令就能完全反转为Java代码,把自己当成一个“反编译工具”(除非你真的已经很牛了,自然本书接下来的内容也不适合你),要学会的是通过这种方式可以认知比Java更低一个抽象层次的逻辑,或许有许多问题直接用Java代码不好解释,但是一旦看到虚指令后就一切明了。 在本节,胖哥分别演示String的小代码,和几段数字处理的小程序(延续下第1章的数字游戏)。
String的代码还少吗?第1章就很多了?
没错,胖哥没有必要再来写第1章写过的那些小程序,就用它们来做实验吧。首先来回顾下代码清单1-1的例子(这里仅截图),如下图所示:
图 3-1 代码清单1-1的还原
当时我们提到这个结果是true,并且解释了它是在编译时被优化,现在就用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
185D:java_A>javac –g:vars,lines chapter01/StringTest.java D:java_A>javap -verbose chapter01.StringTest public class chapter01.StringTest extends java.lang.Object minor version: 0 major version: 50 Constant pool: const #1 = Method #6.#21; // java/lang/Object."<init>":()V const #2 = String #22; // ab1 const #3 = Field #23.#24; // java/lang/System.out:Ljava/io/PrintStream; const #4 = Method #25.#26; // java/io/PrintStream.println:(Z)V const #5 = class #27; // chapter01/StringTest const #6 = class #28; // java/lang/Object const #7 = Asciz <init>; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Asciz LineNumberTable; const #11 = Asciz LocalVariableTable; const #12 = Asciz this; const #13 = Asciz Lchapter01/StringTest;; const #14 = Asciz test1; const #15 = Asciz a; const #16 = Asciz Ljava/lang/String;; const #17 = Asciz b; const #18 = Asciz StackMapTable; const #19 = class #29; // java/lang/String const #20 = class #30; // java/io/PrintStream const #21 = NameAndType #7:#8;// "<init>":()V const #22 = Asciz ab1; const #23 = class #31; // java/lang/System const #24 = NameAndType #32:#33;// out:Ljava/io/PrintStream; const #25 = class #30; // java/io/PrintStream const #26 = NameAndType #34:#35;// println:(Z)V const #27 = Asciz chapter01/StringTest; const #28 = Asciz java/lang/Object; const #29 = Asciz java/lang/String; const #30 = Asciz java/io/PrintStream; const #31 = Asciz java/lang/System; const #32 = Asciz out; const #33 = Asciz Ljava/io/PrintStream;; const #34 = Asciz println; const #35 = Asciz (Z)V; { public chapter01.StringTest(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 4: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lchapter01/StringTest; public static void test1(); Code: Stack=3, Locals=2, Args_size=0 0: ldc #2; //String ab1 2: astore_0 3: ldc #2; //String ab1 5: astore_1 6: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream; 9: aload_0 10: aload_1 11: if_acmpne 18 14: iconst_1 15: goto 19 18: iconst_0 19: invokevirtual #4; //Method java/io/PrintStream.println:(Z)V 22: return LineNumberTable: line 7: 0 line 8: 3 line 9: 6 line 10: 22 LocalVariableTable: Start Length Slot Name Signature 3 20 0 a Ljava/lang/String; 6 17 1 b Ljava/lang/String; StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 18 locals = [ class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream, int ] }
好长好长的篇幅啊!
没关系,我们慢慢来看哈!
首先我们看比较靠前的一个部分是:“常量池”(Constant pool),每一项都以“const #数字”开头,这个数字是顺序递增的,通常把它叫做常量池的入口位置,当程序中需要使用到常量池的时候,就会在程序的对应位置记录下入口位置的标识符(在字节码文件中,就像一个列表一样,列表中的每一项存放的内容和长度是不一样的而已)。
根据入口位置肯定是要找某些常量内容,常量内容会分为很多种。在每个常量池项最前面的1个字节,来标志常量池的类型(我们看到的Method、String等等都是经过映射转换后得到的,字节码中本身只会有1个字节来存放)。
找到类型后,接下来就是内容,内容可以是直接存放在这个常量池的入口中,也可能由其它的一个或多个常量池域组合而成,听起来蛮抽象,胖哥来给大家讲几个例子:
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例子1: const #1 = Method #6.#21; // java/lang/Object."<init>":()V 入口位置#1,简称入口#1,代表一个方法入口,方法入口由:入口#6 和 入口#21两者一起组成,中间用了一个“.”。 const #6 = class #28; // java/lang/Object const #21 = NameAndType #7:#8;// "<init>":()V 入口#6为一个class,class是一种引用,所以它引用了入口#28的常量池。 入口#21 代表一个表示名称和类型(NameAndType),分别由入口#7和入口#8组成。 const #7 = Asciz <init>; const #8 = Asciz ()V; const #28 = Asciz java/lang/Object; 入口#7是一个常量池内容,<init>;代表构造方法的意思。 入口#8 也是一个真正的常量,值为()V,代表没有入口参数,返回值为void,将入口#7和入口#8反推到入口#21,就代表名称为构造方法的名称,入口参数个数为0,返回值为void的意思。 入口#28是一个常量,它的值是“java/lang/Object;”,但这只是一个字符串值,反推到入口#6,要求这个字符串代表的是一个类,那么自然代表的类是java.lang.Object。 综合起来就是:java.lang.Object类的构造方法,入口参数个数为0,返回值为void,其实这在const #1后面的备注中已经标识出来了(这在字节码中本身不存在,只是javap工具帮助合并的)。 例子2: const #2 = String #22; // ab1 它代表将会有一个String类型的引用入口,而引用的是入口#22的内容。 const #22 = Asciz ab1; 这里代表常量池中会存放内容ab1。 综合起来就是:一个String对象的常量,存放的值是ab1。 例子3(稍微复杂一点): const #3 = Field #23.#24; // java/lang/System.out:Ljava/io/PrintStream; const #4 = Method #25.#26; // java/io/PrintStream.println:(Z)V 入口#3代表一个属性,这个属性引用了入口#23的类,入口#24的具体属性。 入口#4代表一个方法,引用了入口#25的类,入口#26的具体方法。 const #23 = class #31; // java/lang/System const #24 = NameAndType #32:#33;// out:Ljava/io/PrintStream; const #25 = class #30; // java/io/PrintStream const #26 = NameAndType #34:#35;// println:(Z)V 入口#23 代表一个类(class),它也是一个引用,它引用了入口#31的常量。 入口#24 代表一个名称和类型(NameAndType),分别对应入口#32:#33。 入口 #25 代表一个class类的引用,具体引用到入口#30。 入口 #26 与入口#24类似,也是一个返回值+引用类型对应入口#34:#35。 const #30 = Asciz java/io/PrintStream; const #31 = Asciz java/lang/System; const #32 = Asciz out; const #33 = Asciz Ljava/io/PrintStream;; const #34 = Asciz println; const #35 = Asciz (Z)V; 入口#30 对应常量池的值为:java/io/PrintStream;反推到入口#25,自然代表类java.lang.PrintStream。 入口#31对应常量池的值为:java/lang/System;反推到入口#23,代表类:java.lang.System。 入口#32 对应常量池的值为:out;反推到入口#24,而入口#24要求名称和类型,这里返回的显然是名称。 入口#33 对应常量池的值为:Ljava/io/PrintStream;; 反推到入口#24这里得到了类型,也就是out的类型是java.io.PrintStream。 入口#34 对应常量池的值为:println;反推到入口#26代表名称为println。 入口#35 对应常量池的值为:(Z)V;反推到入口#26代表入口参数为Z(代表boolean类型),返回值类型是V(代表void) 综合来讲要执行的操作就是: 入口#3是获取到java/lang/System类的属性out,out的类型是Ljava/io/PrintStream; 入口#4是调用java/io/PrintStream类的println方法,方法的返回值类型是void,入口类型是boolean。
小伙伴们应该发现到这个常量池仅仅是操作的陈列,还没有真正的开始执行任务,那么自然就要开始看第2部分的内容,它通过指令将这些内容组合起来。从输出的结果来看,这些的指令是按照方法分开的(其实前面应当还有属性列表),首先看第一个方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public chapter01.StringTest(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 4: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lchapter01/StringTest;
这是一个构造方法,程序中我们没有写构造方法,但是Java自己会帮我们生成一个,说明这个动作是在编译时完成的。虽然是构造方法,但是它足够简单,所以我们先从它开始来说,请看胖哥的解释:
Stack=1, Locals=1, Args_size=1 这一行是所有的方法都会有的,其中Stack代表栈顶的单位大小(每一个大小为一个solt的大小,每个solt是4个字节的宽度),当一个数据需要使用时首先会被放入到栈顶,使用完后会写回到本地变量或主存中。这里的栈的宽度是1,其实是代表有一个this将会被使用。 Locals是本地变量的slot个数,但是并不代表是stack宽度一致,本地变量是在这个方法生命周期内,局部变量最多的时候,需要多大的宽度来存放数据(double、long会占用两个slot)。 Args_size代表的是入参的个数,不再是slot的个数,也就是传入一个long,也只会记录1。 0: aload_0 首先第一个0代表虚指令中的行号(后面会应到,确切说应该是方法的body部分第几个字节),每个方法从0开始顺序递增,但是可以跳跃,跳跃的原因在于一些指令还会接操作的内容,这些操作的内容可能来自常量池,也可以标志是第几个slot的本地变量,因此需要占用一定的空间。 aload_0指令是将“第1个”slot所在的本地变量推到栈顶,并且这个本地变量是引用类型的,相关的指令有:aload_[0-3](范围是:0x2a ~ 0x2d)。如果超过4个,则会使用“aload + 本地变量的slot位置”来完成(此时会多占用1个字节来存放),前者是通过具体的几个指令直接完成。 许多地方会解释为第1个引用类型的本地变量,但胖哥是一个逻辑怪,认为这句话有问题,并不是第1个引用变量,普通变量如果在它之前,它也不是第1个了,此时本身就是第1个本地变量,更确切地说是第一个slot所在位置的本地变量。 1: invokespecial #1; //Method java/lang/Object."<init>":()V 指令中的第2个行号,执行invokespecial指令,这个指令是当发生构造方法调用、父类的构造方法调用、非静态的private方法调用会使用该指令,这里需要从常量池中获取一个方法,这个地方会占用2个字节的宽度,加上指令本身就是3个字节,因此下一个行号是4。 4: return 最后一行是一个return,我们虽然没有自己写return,但是JVM中会自动在编译时加上。 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lchapter01/StringTest; 代表本地变量的列表,这里代表本地变量的作用域起始位置为0,作用域宽度为5(0-4),slot的起始位置也是0,名称为this,类型为chapter01.StringTest。 |
看了构造方法后,如果你理解了,再来看test1方法或许我们会轻松一点,不过大家可以在这个时候先养一养神,再来看哦。胖哥对于细节就不再一一讲述,就在指令后面写备注即可:
public static void test1(); Code: Stack=3, Locals=2, Args_size=0 //Stack=3代表本地栈slot个数为3,两个String需要load,System的out也会占用一个,当发生对比生成boolean的时候,会将两个String的引用从栈顶pop出来,所以栈最多3个slot //Locals为2,因为只有两个String //如果是非静态方法本地变量会自动增加this. //Args_size为0代表这个方法没有任何入口参数 0: ldc #2; //String ab1 //指令body部分从第0个字节为Idc指令,从常量池入口#2中取出内容推到栈顶 //这里的String也是引用,但是它是常量,所以是用Idc指令,不是aload指令 2: astore_0 //将栈顶的引用值,写入第1个slot所在的本地变量中。 //它与aload指令正好相反,对应astore_[0-3](范围是0x4b、0x4e) //更多的本地引用变量写入则使用atore + 引用变量的slot位置。 3: ldc #2; //String ab1 //与第0行一致的操作,引用常量池入口#2来获得 5: astore_1 //类似第2行,将栈顶的值赋值给第2个slot位置的本地引用变量。 6: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream; //获取静态域,放入栈顶,引用了常量池入口#3来获得 //此时的静态区域是System类中的out对象 9: aload_0 //将第1个slot所在位置的本地引用变量加载到栈顶 10: aload_1 //将第二个slot所在位置的本地引用变量加载到栈顶 11: if_acmpne 18 14: iconst_1 15: goto 19 18: iconst_0 //判定两个栈顶的引用是否一致(引用值也就是地址),对比处理的结束位置是18行 // if_acmpne操作之前会先将两个操作数从栈顶pop出来,因此栈顶最多3位 //如果一致则将常量值1写入到栈顶,也就是对应到boolean值true,并跳转到19行 //如果不一致则将常量值0写入到栈顶,对应到boolean值false 19: invokevirtual #4; //Method java/io/PrintStream.println:(Z)V //执行out对象的println方法,方法的入口参数是boolean类型,返回值是void。 //从常量池入口#4获得方法的内容实体。 //此时会将栈顶的元素当成入口参数,栈顶的0或1则会转换为boolean值的true、false。 22: return LineNumberTable: line 7: 0 line 8: 3 line 9: 6 line 10: 22 //对应源文件行号,左边的是字节码的位置(也可以叫做行号),右边的是源文件中的实际文本行号 //javac编译默认有这个内容,但是如果-g:none则不会产生,那么调试就会有问题 LocalVariableTable: Start Length Slot Name Signature 3 20 0 a Ljava/lang/String; 6 17 1 b Ljava/lang/String; //本地变量列表,javac中需要使用-g:vars才会生成,使用一些工具会自动生成,若没有,则调试的时候,断点中看到的变量是没有名称的。 //第一个本地变量的作用区域从第3个字节的位置开始,作用区域范围为20个字节,所在slot的位置是第0个位置,名称为a,类型为java.lang.String。 //第二个本地变量也是类似的方式可以得到结果。 |
在这里,还有一些内容并没有细化,例如StackMapTable的内容,这些请在研究清楚现有的内容后,就可以自己继续去深入和细化了,因为这部分内容会包含的知识是非常多的,关于指令部分,大家可以参考官方文档的介绍来学习。
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-7.html
我们回过头来看问题,为何会输出true就很简单了,第一个变量a,代码中本身编写的是”a” + “b” + 1的操作,但是在常量池中却找不到这3个值,而且指令中也看不到对它们的操作,指令中只看到了对字符串”ab1”的操作,因此在编译阶段,JVM就将它合并了,这样我们不用去听别人说怎么优化,看看便知道。
这样貌似就是去找一些钻牛角尖的问题?
其实不然,其实是帮我们从根本上去了解一些细节,或者说是相对抽象层次较低的细节,当然可能你平时用不上,当我们真的有一天遇到一些诡异的问题,就可能用得上了。
转载自:http://blog.csdn.net/xieyuooo/article/details/17452383
最后
以上就是健康火龙果最近收集整理的关于javap 浅析(实例分析)的全部内容,更多相关javap内容请搜索靠谱客的其他文章。
发表评论 取消回复