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就将它合并了,这样我们不用去听别人说怎么优化,看看便知道。
这样貌似就是去找一些钻牛角尖的问题?
其实不然,其实是帮我们从根本上去了解一些细节,或者说是相对抽象层次较低的细节,当然可能你平时用不上,当我们真的有一天遇到一些诡异的问题,就可能用得上了。为此,胖哥再来个例子。
神马例子呢?很好奇哦!
第1章我们玩了点数字游戏,也许大家没有玩爽,当时胖哥说这一章会有,这一次我们来看那看一个简单的数字操作的指令细节是什么。在这之前,我们先看看代码如下所示(代码是放在第1章内的,但是说明问题是在本章开始说明):
public static void test() { int a = 1 , b = 1 , c = 1 , d = 1;
a++; ++b;
d = ++d;
System.out.println(a + "\t" + b + "\t" + c + "\t" + d); } |
执行结果你猜到了吗,还是你确认了结果!
我们一起来看看输出结果吧:
2 2 1 2 |
胖哥此时估计有的小伙伴也惊呆了,为何会有一个1呢?
其余的几个结果为2的答案很好解释,但是这个1这个答案怎么解释呢?
教科书上通常告诉我们:i++是先做操作再自增,而++i是先自增再做操作。好的,我们按照这种思路来理解下c = c++;这条代码,如果是先做操作,那么这里只有赋值操作,就是c赋值给c,再自增显然自增后应该是2,但是输出的结果1,解释不通。难道是先自增在赋值?如果是这样的话,结果也应该是2才对。
小伙伴们迷茫了。这TNND的到底是怎么回事呢?有的小伙伴可能会说这是多么钻牛角尖的问题啊。胖哥也是这么认为的,这样的问题或许结果并不重要,重要的是它可以让我们了解到一个简单的自增操作不止一个步骤来完成的,让我们真正拥有一种去探索知识内在的兴趣。
教科书上的说法仅仅是为了方便大家理解而给出的一种通用说法,每一种语言在实现它的时候,都有自己的实现方式,我们是Java程序员,自然需要知道Java程序是怎么处理它的了(否则我们就真的就不专业了哦)。
难道自己思考是怎么回事吗,其实这种思考就是猜测了哦,猜测下可以锻炼下猜测能力,不过最终还得了解本质,看看这一小节告诉我们的指令就知道啦,就用它来输出指令看看指令里面到底做了什么(篇幅所限,这里不再看常量池,只说指令,而且只说关键部分)。
public static void test(); Code: Stack=3, Locals=4, Args_size=0 0: iconst_1 //将int类型常量值1推送到栈顶 1: istore_0 //将栈顶抛出赋值给第1个slot所在的int类型本的变量中 2: iconst_1 //与第0行一致 3: istore_1 //将栈顶抛出赋值给第2个slot所在的int类型本的变量中 4: iconst_1 //与第0行一致 5: istore_2 //将栈顶抛出赋值给第3个slot所在的int类型本的变量中 6: iconst_1 //与第0行一致 7: istore_3 //将栈顶抛出赋值给第4个slot所在的int类型本的变量中 8: iinc 0, 1 //将第1个slot所在的int类型本的变量自加1 11: iinc 1, 1 //将第2个slot所在的int类型本的变量自加1 14: iload_2 //将第3个slot所在的int类型本的变量放入栈顶 15: iinc 2, 1 //将第3个slot所在的int类型本的变量加1 18: istore_2 //从栈顶抛出数据写入到第3个slot所在的int类型本的变量 19: iinc 3, 1 //将第4个slot位置所在的int类型的本变量自增1 22: iload_3 //将第4个slot位置所在的int类型的本地变量加载到栈顶 23: istore_3 //将栈顶数据抛出,写入到第4个slot所在的int类型的本地变量中 LocalVariableTable: Start Length Slot Name Signature 2 70 0 a I //本地变量a,类型int,作用域第2行开始,作用域范围70行 4 68 1 b I//本地变量b,类型int,作用域第4行开始,作用域范围68行 6 66 2 c I//本地变量c,类型int,作用域第6行开始,作用域范围66行 8 64 3 d I//本地变量d,类型int,作用域第8行开始,作用域范围64行 |
现在我们来逐步看问题,首先发现的第一个特征是第8行、第11行,它们都做了iinc操作,都是对本地变量做叠加操作,分别是对前面两个本地变量(a、b)做叠加操作,后续没有其它的动作。换句话说,当一个本地变量发生i++或++i的操作的时候,如果这个代码发生在单行上面,即不会用于其它的计算操作,它们最终的指令都是iinc,也就是i++也会被改为++i操作。
进一步来看第3个本地变量c的操作,首先是通过iload_2指令将其拷贝到栈顶,然后发生iinc操作(即自增操作),然后通过istore_2指令将栈顶的数据赋值给这个本地变量,因此,你可以认为它就像做了一个这样的操作:
int tmp = c; c++; c= tmp;
这样3个步骤的动作,只是这个tmp并不是真实存在的本地变量,而是栈顶的一份数据拷贝,这一份拷贝的数据其实是为其它的操作,而自己叠加数据并不参与其它的计算,这才是Java中实现i++的真实道理。
对比d的操作,可以看到d是先进行了iinc操作,然后再做iload、istore的两个动作用于赋值的,所以d是会被叠加的,只是最后两个动作是多余的而已。
这样小伙伴们是不是有点晕了!
我们画个图来看看,或许你会清楚一点。
首先来看看,进入方法前,JVM分配的栈大概是什么样子的(这个部分不包含指令及指令中指向的常量池位置):
图 3-2 初始化一个方法后,大概是这样的哦
当iconst_1发生的时候,结构就发生改变了:
当istore_0发生操作的时候,将栈顶抛出,赋值给变量a,此时的结构变成这样:
以此类推,发生到第7行,对4个本地变量都会发生这样的赋值,结果为:
图 3-5 分别通过栈定赋值后的结果,栈顶只用了一个slot
iinc指令我们没有必要讲解(实现的细节也可以是利用了一个栈顶来store、叠加1、load),总之a、b两个变量变成了2。当再进一步做c = c++操作的时候首先发生第1个步骤是将数据拷贝到栈顶,然后将本地变量改为2,然后再从栈顶拷贝回来,如下图所示:
图 3-6 c=c++操作的程序运行过程
小伙伴们看懂了,但是又有的小伙伴着急了:后进先出栈明明只用一个slot为什么会有3个呢?
能问出这个问题说明你懂得思考,其实刚开始我们只是输出了一些简单的操作指令,后来还有一条代码System.out.println(a+ "\t" + b + "\t" + c + "\t" + d);相关指令还没有输出呢。别看这就一行代码,指令可多了哦(写代码写得短,并不代表指令短,也就是不能代表跑得快),一起来看看接下来的一些指令:
24: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 27: new #3; //class java/lang/StringBuilder 30: dup 31: invokespecial #4; //Method java/lang/StringBuilder."<init>":()V 34: iload_0 35: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 38: ldc #6; //String \t 40: invokevirtual #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 43: iload_1 44: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 47: ldc #6; //String \t 49: invokevirtual #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 52: iload_2 53: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 56: ldc #6; //String \t 58: invokevirtual #7;//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 61: iload_3 62: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 65: invokevirtual #8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 68: invokevirtual #9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 71: return |
好长,其实大部分指令都是invokevirtual指令,关键它在操作什么。其实大伙看看后面的注释应该就看懂了(这也是刚开始看不懂虚指令,可以直接看注释的方式)。现在我们想要知道本地栈要使用3个slot怎么来的。
首先getstatic指令要将System类的out这个静态属性获取出来放入栈顶(因为没有局部变量存放,只能放在栈顶),接着通过new指令创建一个对象,这个对象通过常量池入口#3获得是一个StringBuilder类型(这是证明第1章中提到的字符串拼接的结论)。此时栈的样子应当是这样的:
图 3-7 执行前两条指令后,栈空间的情况
此时发生的是dup命令,它会拷贝一份栈顶的内容,并写入栈顶,为什么要这样做呢?因为后续的invokespecial操作将会栈顶的信息抛出执行,执行StringBuilder的构造方法(小伙伴们又着急了,不是已经创建了吗,干嘛还有构造方法,其实刚才的new仅仅分配了空间,还没有对内容进行初始化呢,一个简单的创建对象其实需要多条指令来完成的)。因此此时的栈就变成了这样的情况:
图3-8 执行dup指令过后的情况
这里3个栈就用过了,小伙伴们应该清楚了Stack为3的情况了吧!大家可以将这条代码去掉,或自己用一个StringBuilder来拼接看看结果是什么样子的。
接下来就将栈顶抛出,执行StringBuilder的构造方法(两个引用其实引用同一个对象),初始化后就变得和图3-7一样,只是现在的StringBuilder已经执行完构造方法(但是并不代表所有属性都初始化完成,在第5章会提到重排序的问题)。
紧接着,将本地变量、常量“\t”逐个iload或aload到栈顶,然后调用invokevirtual指令调用StringBuilder类的append方法,虽然它也会pop出来做操作,但这个方法会有一个StringBuilder返回值,由于下一个动作是基于这个返回值来操作,所以这个返回值将会再次被赋值到栈顶,因此它执行前无需再拷贝了,如果这个StringBuilder是一个自定义的本地变量,也无需再一次iload操作。
大家可以在这段代码上做几个小改动,进一步分析: ○ 将拼接过程换成一个StringBuilder,看看Stacks的数量有没有变化。 ○ 换成一个StringBuilder在一行代码中append多个变量,与分成多行分别append,指令上是否有区别(这里append的内容有7个,你完全可以拆分2、3个出来看看)。 ○ 添加一个自定义对象,自定义对象中有一个void返回值的方法,也像append那样反复调用,看看它是不是需要每次iload,而StringBuilder不需要。其实为什么我们已经解释过了,接下来就靠大家自己去扩展了哦。 |
胖哥只是举例说明一些简单的例子,大家可以继续扩展,例如(i++) + (++i) + (i++)等等,或许你看看指令就清楚了内在的执行顺序。关于JVM的指令有200多个,我们要一一看完不容易,可以先看自己想看的一些指令,或者自己写几个简单程序看看指令。等到我们知道了许多的指令后,再系统化的看这些指令,就很轻松了哦。
这些指令还是javap命令告诉我们的,javap命令本身也将字节码翻译成了文字,它比起反编译工具只是更加接近于字节码的结构(大家也大概了解到反编译工具就是基于这种指令反向计算出程序代码的),但是它还不是真正的字节码,如果有兴趣的小伙伴们,可以看看下一节胖哥对于字节码本身的介绍,然后javap命令工具是如何解析这个字节码得到内容的。
这是本书的一个小样章,内容格式贴进来全部乱了,请大家谅解。
哈哈,最后还是插播一个广告:
大家觉得小胖的文章写得还行的话,就投票吧,哈哈!
投票地址:http://vote.blog.csdn.net/blogstaritem/blogstar2013/xieyuooo
呵呵,觉得想吐槽就吐吧!