一.基本命令及原始代码
本文分析使用的源代码如下:
1 public classStringTest{2 public static voidmain(String[] args){3 String a = "a" + "b" + 1;4 String b = "ab1";5 System.out.println(a ==b);6 }7 }
使用javap命令进行反编译:
1 javac -g:vars,lines StringTest.java2
3 javap -verbose StringTest
反编译的结果如下:
1 public classStringTest2 minor version: 0
3 major version: 52
4 flags: ACC_PUBLIC, ACC_SUPER5 Constant pool:6 #1 = Methodref #6.#25 //java/lang/Object."":()V
7 #2 = String #26 //ab1
8 #3 = Fieldref #27.#28 //java/lang/System.out:Ljava/io/PrintStream;
9 #4 = Methodref #29.#30 //java/io/PrintStream.println:(Z)V
10 #5 = Class #31 //StringTest
11 #6 = Class #32 //java/lang/Object
12 #7 = Utf8
13 #8 =Utf8 ()V14 #9 =Utf8 Code15 #10 =Utf8 LineNumberTable16 #11 =Utf8 LocalVariableTable17 #12 = Utf8 this
18 #13 =Utf8 LStringTest;19 #14 =Utf8 main20 #15 = Utf8 ([Ljava/lang/String;)V21 #16 =Utf8 args22 #17 = Utf8 [Ljava/lang/String;23 #18 =Utf8 a24 #19 = Utf8 Ljava/lang/String;25 #20 =Utf8 b26 #21 =Utf8 StackMapTable27 #22 = Class #17 //"[Ljava/lang/String;"
28 #23 = Class #33 //java/lang/String
29 #24 = Class #34 //java/io/PrintStream
30 #25 = NameAndType #7:#8 //"":()V
31 #26 =Utf8 ab132 #27 = Class #35 //java/lang/System
33 #28 = NameAndType #36:#37 //out:Ljava/io/PrintStream;
34 #29 = Class #34 //java/io/PrintStream
35 #30 = NameAndType #38:#39 //println:(Z)V
36 #31 =Utf8 StringTest37 #32 = Utf8 java/lang/Object38 #33 = Utf8 java/lang/String39 #34 = Utf8 java/io/PrintStream40 #35 = Utf8 java/lang/System41 #36 =Utf8 out42 #37 = Utf8 Ljava/io/PrintStream;43 #38 =Utf8 println44 #39 =Utf8 (Z)V45 {46 publicStringTest();47 descriptor: ()V48 flags: ACC_PUBLIC49 Code:50 stack=1, locals=1, args_size=1
51 0: aload_052 1: invokespecial #1 //Method java/lang/Object."":()V
53 4: return
54 LineNumberTable:55 line 1: 0
56 LocalVariableTable:57 Start Length Slot Name Signature58 0 5 0 thisLStringTest;59
60 public static voidmain(java.lang.String[]);61 descriptor: ([Ljava/lang/String;)V62 flags: ACC_PUBLIC, ACC_STATIC63 Code:64 stack=3, locals=3, args_size=1
65 0: ldc #2 //String ab1
66 2: astore_167 3: ldc #2 //String ab1
68 5: astore_269 6: getstatic #3 //Field java/lang/System.out:Ljava/io/PrintStream;
70 9: aload_171 10: aload_272 11: if_acmpne 18
73 14: iconst_174 15: goto 19
75 18: iconst_076 19: invokevirtual #4 //Method java/io/PrintStream.println:(Z)V
77 22: return
78 LineNumberTable:79 line 3: 0
80 line 4: 3
81 line 5: 6
82 line 6: 22
83 LocalVariableTable:84 Start Length Slot Name Signature85 0 23 0 args [Ljava/lang/String;86 3 20 1 a Ljava/lang/String;87 6 17 2 b Ljava/lang/String;88 StackMapTable: number_of_entries = 2
89 frame_type = 255 /*full_frame*/
90 offset_delta = 18
91 locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String ]92 stack = [ class java/io/PrintStream ]93 frame_type = 255 /*full_frame*/
94 offset_delta = 0
95 locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String ]96 stack = [ class java/io/PrintStream, int]97 }
二. 字节码分析
1.常量池分析
首先我们看比较靠前的一个部分是:“常量池”(Constant pool),每一项都以“const #数字”开头,这个数字是顺序递增的,通常把它叫做常量池的入口位置,当程序中需要使用到常量池的时候,就会在程序的对应位置记录下入口位置的标识符(在字节码文件中,就像一个列表一样,列表中的每一项存放的内容和长度是不一样的而已)。
根据入口位置肯定是要找某些常量内容,常量内容会分为很多种。在每个常量池项最前面的1个字节,来标志常量池的类型(我们看到的Method、String等等都是经过映射转换后得到的,字节码中本身只会有1个字节来存放)。
找到类型后,接下来就是内容,内容可以是直接存放在这个常量池的入口中,也可能由其它的一个或多个常量池域组合而成,听起来蛮抽象 ,下面通过例子来说明:
第一部分:
1 Constant pool:2 #1 = Methodref #6.#25 //java/lang/Object."":()V
3 入口位置#1,简称入口#1,代表一个方法入口,方法入口由:入口#6和 入口#25两者一起组成,中间用了一个“.”。4 #2 = String #26 //ab1
5 #3 = Fieldref #27.#28 //java/lang/System.out:Ljava/io/PrintStream;
6 #4 = Methodref #29.#30 //java/io/PrintStream.println:(Z)V
7 #5 = Class #31 //StringTest
8 #6 = Class #32 //java/lang/Object9 //1>. 入口#6为一个Class对象(java/lang/Object),所以它引用了入口#32的常量池。
10 #7 = Utf8
11 #8 =Utf8 ()V12 //2>. 入口#7是一个常量池内容,;代表构造方法的意思。13 //3>. 入口#8 也是一个真正的常量,值为()V,代表没有入口参数,返回值为void,14 //4>. 将入口#7和入口#8反推到入口#32,就代表名称为构造方法的名称,入口参数个数为0,返回值为void的意思。
15 入口#32是一个常量,它的值是“java/lang/Object;”,但这只是一个字符串值,反推到入口#6,16 //5>. 入口#6要求这个字符串代表的是一个类,那么自然代表的类是java.lang.Object。17 //6>. 综合起来就是:java.lang.Object类的构造方法,入口参数个数为0,返回值为void,
18 其实这在const #1后面的备注中已经标识出来了(这在字节码中本身不存在,只是javap工具帮助合并的)。
第二部分:
1 #2 = String #26 //ab12 //1>. 代表这是一个String类型的引用入口,本入口引用的内容为入口#26的值
3 #26 =Utf8 ab14 //1>. 代表当前常量池中存放的内容为ab15 //2>.综合起来就是:一个String对象的常量,存放的值为ab1
第三部分:
1 #3 = Fieldref #27.#28 //java/lang/System.out:Ljava/io/PrintStream;
2
3 #27 = Class #35 //java/lang/System
4 #35 = Utf8 java/lang/System5 #28 = NameAndType #36:#37 //out:Ljava/io/PrintStream;
6 #36 =Utf8 out7 #37 = Utf8 Ljava/io/PrintStream;8
9
10
11 #4 = Methodref #29.#30 //java/io/PrintStream.println:(Z)V
12
13 #29 = Class #34 //java/io/PrintStream
14 #34 = Utf8 java/io/PrintStream15 #30 = NameAndType #38:#39 //println:(Z)V
16 #38 =Utf8 println17 #39 =Utf8 (Z)V18
19 //1>. 入口#3是获取到java/lang/System类的属性out,out的类型是Ljava/io/PrintStream;20
21 //2>. 入口#4是调用java/io/PrintStream类的println方法,方法的返回值类型是void,入口类型是boolean。
2.方法分析
方法一(构造函数):
1 publicStringTest();2 descriptor: ()V3 flags: ACC_PUBLIC4 Code:5 stack=1, locals=1, args_size=1
6 0: aload_07 1: invokespecial #1 //Method java/lang/Object."":()V
8 4: return
9 LineNumberTable:10 line 1: 0
11 LocalVariableTable:12 Start Length Slot Name Signature13 0 5 0 this LStringTest;
这是一个构造方法,程序中我们没有写构造方法,但是Java自己会帮我们生成一个,说明这个动作是在编译时完成的。虽然是构造方法,但是它足够简单,所以我们先从它开始来说 :
stack=1, locals=1, args_size=1
这一行是所有方法都会有的,其中stack代表栈的深度(单位大小为一个slot的大小),当一个数据被使用时会被先放到栈顶,使用完回写到本地变量或主存中,这里栈的深度为1,代表有一个this将会被使用。
locals是本地变量的slot个数,但是并不代表是stack宽度一致,本地变量是在这个方法生命周期内,局部变量最多的时候,需要多大的宽度来存放数据(double、long会占用两个slot)。
args_size代表的是入参的个数。
0: aload_0
首先第一个0代表虚指令中的行号(后面会应到,确切说应该是方法的body部分第几个字节),每个方法从0开始顺序递增,但是可以跳跃,跳跃的原因在于一些指令还会接操作的内容,这些操作的内容可能来自常量池,也可以标志是第几个slot的本地变量,因此需要占用一定的空间。
aload_0指令是将“第1个”slot所在的本地变量推到栈顶,并且这个本地变量是引用类型的,相关的指令有:aload_[0-3](范围是:0x2a ~ 0x2d)。如果超过4个,则会使用“aload + 本地变量的slot位置”来完成(此时会多占用1个字节来存放),前者是通过具体的几个指令直接完成。
1: invokespecial #1 // Method java/lang/Object."":()V
指令中的第2个行号,执行invokespecial指令,这个指令是当发生构造方法调用、父类的构造方法调用、非静态的private方法调用会使用该指令,这里需要从常量池中获取一个方法,这个地方会占用2个字节的宽度,加上指令本身就是3个字节,因此下一个行号是4。
4: return
最后一行是一个return,我们虽然没有自己写return,但是JVM中会自动在编译时加上。
最后是局部变量表:
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LStringTest;
代表本地变量的列表,这里代表本地变量的作用域起始位置为0,作用域宽度为5(0-4),slot的起始位置也是0,名称为this,类型为StringTest。
方法二:
1 public static voidmain(java.lang.String[]);2 descriptor: ([Ljava/lang/String;)V3 flags: ACC_PUBLIC, ACC_STATIC4 Code:5 stack=3, locals=3, args_size=1
6 0: ldc #2 //String ab1
7 /**
8 1>.从常量入口#2取出内容推到栈顶,这里的string也是引用,但它是常量,所以用ldc,不是aload指令9 */
10 2: astore_111 /**
12 2>.将栈顶的引用值,写入第一个slot所在的本地变量中(a)13 */
14 3: ldc #2 //String ab1
15 5: astore_216 6: getstatic #3 //Field java/lang/System.out:Ljava/io/PrintStream;
17 /**
18 3>.获取静态域放入栈顶,引用了常量池入口#3来获得,此时静态区域是System类中的out对象。19 */
20 9: aload_121 10: aload_222 /**
23 4>.分别将a、b加载至栈顶24 */
25 11: if_acmpne 18
26 14: iconst_127 15: goto 19
28 18: iconst_029 19: invokevirtual #4 //Method java/io/PrintStream.println:(Z)V
30 /**
31 3>.判断两个栈顶的引用是否一致(地址),对比处理的结束位置是第18行。32 if_acmpne操作会将之前的两个操作数从栈顶pop出来,因此栈顶最多只有3个33 如果一致则将常量值_1写入栈顶,也就是true34 如果不一致则将常量值_0写入栈顶,也就是false35 */
36 22: return
37 LineNumberTable:38 line 3: 0
39 line 4: 3
40 line 5: 6
41 line 6: 22
42 LocalVariableTable:43 Start Length Slot Name Signature44 0 23 0 args [Ljava/lang/String;45 3 20 1 a Ljava/lang/String;46 6 17 2 b Ljava/lang/String;