一.基本命令及原始代码
本文分析使用的源代码如下:
1 public class StringTest{ 2 public static void main(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.java 2 3 javap -verbose StringTest
反编译的结果如下:
1 public class StringTest 2 minor version: 0 3 major version: 52 4 flags: ACC_PUBLIC, ACC_SUPER 5 Constant pool: 6 #1 = Methodref #6.#25 // java/lang/Object."<init>":()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 <init> 13 #8 = Utf8 ()V 14 #9 = Utf8 Code 15 #10 = Utf8 LineNumberTable 16 #11 = Utf8 LocalVariableTable 17 #12 = Utf8 this 18 #13 = Utf8 LStringTest; 19 #14 = Utf8 main 20 #15 = Utf8 ([Ljava/lang/String;)V 21 #16 = Utf8 args 22 #17 = Utf8 [Ljava/lang/String; 23 #18 = Utf8 a 24 #19 = Utf8 Ljava/lang/String; 25 #20 = Utf8 b 26 #21 = Utf8 StackMapTable 27 #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 // "<init>":()V 31 #26 = Utf8 ab1 32 #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 StringTest 37 #32 = Utf8 java/lang/Object 38 #33 = Utf8 java/lang/String 39 #34 = Utf8 java/io/PrintStream 40 #35 = Utf8 java/lang/System 41 #36 = Utf8 out 42 #37 = Utf8 Ljava/io/PrintStream; 43 #38 = Utf8 println 44 #39 = Utf8 (Z)V 45 { 46 public StringTest(); 47 descriptor: ()V 48 flags: ACC_PUBLIC 49 Code: 50 stack=1, locals=1, args_size=1 51 0: aload_0 52 1: invokespecial #1 // Method java/lang/Object."<init>":()V 53 4: return 54 LineNumberTable: 55 line 1: 0 56 LocalVariableTable: 57 Start Length Slot Name Signature 58 0 5 0 this LStringTest; 59 60 public static void main(java.lang.String[]); 61 descriptor: ([Ljava/lang/String;)V 62 flags: ACC_PUBLIC, ACC_STATIC 63 Code: 64 stack=3, locals=3, args_size=1 65 0: ldc #2 // String ab1 66 2: astore_1 67 3: ldc #2 // String ab1 68 5: astore_2 69 6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 70 9: aload_1 71 10: aload_2 72 11: if_acmpne 18 73 14: iconst_1 74 15: goto 19 75 18: iconst_0 76 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 Signature 85 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."<init>":()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/Object 9 //1>. 入口#6为一个Class对象(java/lang/Object),所以它引用了入口#32的常量池。 10 #7 = Utf8 <init> 11 #8 = Utf8 ()V 12 //2>. 入口#7是一个常量池内容,<init>;代表构造方法的意思。 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 // ab1 2 //1>. 代表这是一个String类型的引用入口,本入口引用的内容为入口#26的值 3 #26 = Utf8 ab1 4 //1>. 代表当前常量池中存放的内容为ab1 5 //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/System 5 #28 = NameAndType #36:#37 // out:Ljava/io/PrintStream; 6 #36 = Utf8 out 7 #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/PrintStream 15 #30 = NameAndType #38:#39 // println:(Z)V 16 #38 = Utf8 println 17 #39 = Utf8 (Z)V 18 19 //1>. 入口#3是获取到java/lang/System类的属性out,out的类型是Ljava/io/PrintStream; 20 21 //2>. 入口#4是调用java/io/PrintStream类的println方法,方法的返回值类型是void,入口类型是boolean。
2.方法分析
方法一(构造函数):
1 public StringTest(); 2 descriptor: ()V 3 flags: ACC_PUBLIC 4 Code: 5 stack=1, locals=1, args_size=1 6 0: aload_0 7 1: invokespecial #1 // Method java/lang/Object."<init>":()V 8 4: return 9 LineNumberTable: 10 line 1: 0 11 LocalVariableTable: 12 Start Length Slot Name Signature 13 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."<init>":()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 void main(java.lang.String[]); 2 descriptor: ([Ljava/lang/String;)V 3 flags: ACC_PUBLIC, ACC_STATIC 4 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_1 11 /** 12 2>.将栈顶的引用值,写入第一个slot所在的本地变量中(a) 13 */ 14 3: ldc #2 // String ab1 15 5: astore_2 16 6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 17 /** 18 3>.获取静态域放入栈顶,引用了常量池入口#3来获得,此时静态区域是System类中的out对象。 19 */ 20 9: aload_1 21 10: aload_2 22 /** 23 4>.分别将a、b加载至栈顶 24 */ 25 11: if_acmpne 18 26 14: iconst_1 27 15: goto 19 28 18: iconst_0 29 19: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V 30 /** 31 3>.判断两个栈顶的引用是否一致(地址),对比处理的结束位置是第18行。 32 if_acmpne操作会将之前的两个操作数从栈顶pop出来,因此栈顶最多只有3个 33 如果一致则将常量值_1写入栈顶,也就是true 34 如果不一致则将常量值_0写入栈顶,也就是false 35 */ 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 Signature 44 0 23 0 args [Ljava/lang/String; 45 3 20 1 a Ljava/lang/String; 46 6 17 2 b Ljava/lang/String;