概述
3.2.1javap命令工具
第1章中我们就提到了有些地方需要用javap命令工具来看编译后的指令是什么,第2.2.1节中胖哥使用了一个简单的程序让大家感受了一下javap命令工具是什么,这里再次谈到javap命令工具了。或许这一次我们可以对javap命令工具说得稍微清楚一点。为此,胖哥会单独再写几段小程序给大家说说javap命令工具的结果怎么看。
胖哥为什么要给简单程序呢?为啥不直接来个复杂的程序呢?
答曰:javap命令工具输出的内容是繁杂的,即使是一段小程序输出后,结果也比原始代码要复杂很多。我们要学的其实并不是说看指令就能完全反转为Java代码,把自己当成一个“反编译工具”(除非你真的已经很牛了,自然本书接下来的内容也不适合你),要学会的是通过这种方式可以认知比Java更低一个抽象层次的逻辑,或许有许多问题直接用Java代码不好解释,但是一旦看到虚指令后就一切明了。
在本节,胖哥分别演示String的小代码,和几段数字处理的小程序(延续下第1章的数字游戏)。
String的代码还少吗?第1章就很多了?
没错,胖哥没有必要再来写第1章写过的那些小程序,就用它们来做实验吧。首先来回顾下代码清单1-1的例子(这里仅截图),如下图所示:
图 3-1 代码清单1-1的还原
当时我们提到这个结果是true,并且解释了它是在编译时被优化,现在就用javap指令来论证下这个结论吧:
D: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:
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部分的内容,它通过指令将这些内容组合起来。从输出的结果来看,这些的指令是按照方法分开的(其实前面应当还有属性列表),首先看第一个方法:
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;
这是一个构造方法,程序中我们没有写构造方法,但是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 浅析(实例分析)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复