关于JVM中符号引用的理解,小白专场。。。。
public class Tests10 {
public static void main(String[] args) {
Sum sum = new Sum();
int a = 40;
int b = 60;
int c = sum.add(a,b);
System.out.println(c);
}
}
class Sum {
public int add(int a,int b) {
return a + b;
}
}
源文件编译后的.class文件反编译后的类似汇编语言的字节码文件:
Constant pool:
#1 = Class #2 // Tests10
#2 = Utf8 Tests10
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LTests10;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Class #17 // Sum
#17 = Utf8 Sum
#18 = Methodref #16.#9 // Sum."<init>":()V
#19 = Methodref #16.#20 // Sum.add:(II)I
#20 = NameAndType #21:#22 // add:(II)I
#21 = Utf8 add
#22 = Utf8 (II)I
#23 = Fieldref #24.#26 // java/lang/System.out:Ljava/io/PrintStream;
#24 = Class #25 // java/lang/System
#25 = Utf8 java/lang/System
#26 = NameAndType #27:#28 // out:Ljava/io/PrintStream;
#27 = Utf8 out
#28 = Utf8 Ljava/io/PrintStream;
#29 = Methodref #30.#32 // java/io/PrintStream.println:(I)V
#30 = Class #31 // java/io/PrintStream
#31 = Utf8 java/io/PrintStream
#32 = NameAndType #33:#34 // println:(I)V
#33 = Utf8 println
#34 = Utf8 (I)V
#35 = Utf8 args
#36 = Utf8 [Ljava/lang/String;
#37 = Utf8 sum
#38 = Utf8 LSum;
#39 = Utf8 a
#40 = Utf8 I
#41 = Utf8 b
#42 = Utf8 c
#43 = Utf8 SourceFile
#44 = Utf8 Tests10.java
{
public Tests10();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTests10;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: new #16 // class Sum
3: dup
4: invokespecial #18 // Method Sum."<init>":()V
7: astore_1
8: bipush 40
10: istore_2
11: bipush 60
13: istore_3
14: aload_1
15: iload_2
16: iload_3
17: invokevirtual #19 // Method Sum.add:(II)I
20: istore 4
22: getstatic #23 // Field java/lang/System.out:Ljava/io/PrintStream;
25: iload 4
27: invokevirtual #29 // Method java/io/PrintStream.println:(I)V
30: return
LineNumberTable:
line 5: 0
line 6: 8
line 7: 11
line 8: 14
line 9: 22
line 10: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 sum LSum;
11 20 2 a I
14 17 3 b I
22 9 4 c I
}
SourceFile: "Tests10.java"
我们对比src文件和反编译后的文件来看(两者是等价的)
我们从src文件中main方法中看到有个调用了add方法,然后回到反编译后的文件,下面invokevirtual指令是调用方法的意思,后面的#18是字符串常量池的下标。
4: invokespecial #18 // Method Sum."<init>":()V
我们定位到#18这个地方,就得到了这个,
#18 = Methodref #16.#9 // Sum."<init>":()V
根据对虚拟机规范中对Methodref 的定义:
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
主要看这两句,class_index 表示了类的索引,name_and_type_index 表示了名称与类型的索引,这两个也都是常量池的下标,可以看成Methodref 的内容:
u2 class_index;
u2 name_and_type_index;
上面的内容的“地址”分别对应#18 = Methodref后面的#16.#19这两个下标,按#16.#19继续到常量池找(注意:看缩进,就像树结构一样,一层一层找,找到最后的叶子结点就是对应的方法所属类的类名 方法名和和类型),
#16 = Class #17 // Sum
#17 = Utf8 Sum
#19 = Methodref #16.#20 // Sum.add:(II)I
#16 = Class #17 // Sum
#17 = Utf8 Sum
#20 = NameAndType #21:#22 // add:(II)I
#21 = Utf8 add
#22 = Utf8 (II)I
按照此规律如此下去,最终结果如下:
#17 = Utf8 Sum
#21 = Utf8 add
#22 = Utf8 (II)I
看吧,每个字符串常量后面都有连接下一个字符串常量的地址,每个字符串常量看成一个“线索”,这些线索连起来就可以得到描述(如Sum),该例是对调用的方法的描述,它属于Sum的方法,即默认包Sum类下的方法,方法名是add,类型是(II)I。
总结:
反编译后的.class文件就是通过一个一个“线索”(字符串常量)连接起来组成得到的描述,得到的所有描述可以想象成几乎与src文件一模一样的蓝图。同时“地址”看成逻辑地址,用这个在内存中找不到对应程序片段的入口,这只是将逻辑思路连起来方便读者解读,也就是说这些只不过是以字符串的形式描述,真正变成可以到内存中找的地址是在虚拟机中落地变成直接引用。