javap 浅析(实例分析)

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

   //将栈顶的引用值,写入第1slot所在的本地变量中。

//它与aload指令正好相反,对应astore_[0-3](范围是0x4b0x4e

//更多的本地引用变量写入则使用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

   //将第1slot所在位置的本地引用变量加载到栈顶

   10:  aload_1

   //将第二个slot所在位置的本地引用变量加载到栈顶

   11:  if_acmpne       18

   14:  iconst_1

   15:  goto    19

   18:  iconst_0

   //判定两个栈顶的引用是否一致(引用值也就是地址),对比处理的结束位置是18

   // if_acmpne操作之前会先将两个操作数从栈顶pop出来,因此栈顶最多3

   //如果一致则将常量值1写入到栈顶,也就是对应到booleantrue,并跳转到19

//如果不一致则将常量值0写入到栈顶,对应到booleanfalse

   19:  invokevirtual   #4; //Method java/io/PrintStream.println:(Z)V

   //执行out对象的println方法,方法的入口参数是boolean类型,返回值是void

   //从常量池入口#4获得方法的内容实体。

//此时会将栈顶的元素当成入口参数,栈顶的0或1则会转换为boolean值的truefalse

   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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值