java 符号引用_JVM符号引用——个人修炼

本文详细解析了Java Class文件中的符号引用和JVM中的直接引用。从Class文件的常量池出发,展示了如何通过符号引用找到方法调用的实际目标,并解释了在JVM加载和解析过程中,如何将符号引用转换为直接引用,以便于JVM直接执行。文中还对比了元组JVM和HotSpotVM的实现差异,强调了直接引用在执行效率上的优势。
摘要由CSDN通过智能技术生成

先看Class文件里的“符号引用”。考虑这样一个Java类:

public classX {

public voidfoo() {

bar();

}

public voidbar() { }

}

它编译出来的Class文件的文本表现形式如下:

Classfile /D:/IntellijPrj/exp_prj/WebRoot/WEB-INF/classes/priv/hpl/primary/test/X.class

Last modified 2018-9-19; size 416 bytes

MD5 checksum 00f38f1c38e3b9e14ca0fff7745bb981

Compiled from "X.java"

public class priv.hpl.primary.test.X

minor version: 0

major version: 52

flags: ACC_PUBLIC, ACC_SUPER

Constant pool:

#1 = Methodref #4.#16 // java/lang/Object."":()V

#2 = Methodref #3.#17 // priv/hpl/primary/test/X.bar:()V

#3 = Class #18 // priv/hpl/primary/test/X

#4 = Class #19 // java/lang/Object

#5 = Utf8

#6 = Utf8 ()V

#7 = Utf8 Code

#8 = Utf8 LineNumberTable

#9 = Utf8 LocalVariableTable

#10 = Utf8 this

#11 = Utf8 Lpriv/hpl/primary/test/X;

#12 = Utf8 foo

#13 = Utf8 bar

#14 = Utf8 SourceFile

#15 = Utf8 X.java

#16 = NameAndType #5:#6 // "":()V

#17 = NameAndType #13:#6 // bar:()V

#18 = Utf8 priv/hpl/primary/test/X

#19 = Utf8 java/lang/Object

{

public priv.hpl.primary.test.X();

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: invokespecial #1 // Method java/lang/Object."":()V

4: return

LineNumberTable:

line 10: 0

LocalVariableTable:

Start Length Slot Name Signature

0 5 0 this Lpriv/hpl/primary/test/X;

public void foo();

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: invokevirtual #2 // Method bar:()V

4: return

LineNumberTable:

line 12: 0

line 13: 4

LocalVariableTable:

Start Length Slot Name Signature

0 5 0 this Lpriv/hpl/primary/test/X;

public void bar();

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=0, locals=1, args_size=1

0: return

LineNumberTable:

line 15: 0

LocalVariableTable:

Start Length Slot Name Signature

0 1 0 this Lpriv/hpl/primary/test/X;

}

SourceFile: "X.java"

可以看到Class文件里有一段叫做“常量池”,里面存储的该Class文件里的大部分常量的内容。来考察foo()方法里的一条字节码指令:

1: invokevirtual #2 // Method bar:()V

这在Class文件中的实际编码为:

[B6] [00 02]

其中0xB6是invokevirtual指令的操作码(opcode),后面的0x0002是该指令的操作数(operand),用于指定要调用的目标方法。这个参数是Class文件里的常量池的下标。那么去找下标为2的常量池项,是:

#2 = Methodref #3.#17 // X.bar:()V

这在Class文件中的实际编码为(以十六进制表示,Class文件里使用高位在前字节序(big-endian)):

[0A] [00 03] [00 11]

其中0x0A是CONSTANT_Methodref_info的tag,后面的0x0003和0x0011是该常量池项的两个部分:class_index(指向了常量池中索引为class_index的常量项,且这个常量项必须为Class Info类型)和name_and_type_index(指向了常量池中索引为name_and_type_index的常量项,且这个常量项必须为Name And Type Info类型)。这两部分分别都是常量池下标,引用着另外两个常量池项。

顺着这条线索把能传递引用到的常量池项都找出来,会看到(按深度优先顺序排列):

#2 = Methodref #3.#17 // X.bar:()V

#3 = Class #18 // X

#18 = Utf8 X

#17 = NameAndType #13:#6 // bar:()V

#13 = Utf8 bar

#6 = Utf8 ()V

把引用关系画成一棵树的话:

标记为Utf8的常量池项在Class文件中实际为CONSTANT_Utf8_info,是以略微修改过的UTF-8编码的字符串文本。

这样就清楚了对不对?

由此可以看出,Class文件中的invokevirtual指令的操作数经过几层间接之后,最后都是由字符串来表示的。这就是Class文件里的“符号引用”的实态:带有类型/结构(类型即tag,就是CONSTANT_Methodref_info,结构即符号间引用层次)的字符串。

==================================================

然后再看JVM里的“直接引用”的样子。

这里就不拿HotSpot VM来举例了,因为它的实现略复杂。让我们看个更简单的实现,Sun的元祖JVM——Sun JDK 1.0.2的32位x86上的做法。

请先参考另一个回答里讲到Sun Classic VM的部分:为什么bs虚函数表的地址(int*)(&bs)与虚函数地址(int*)*(int*)(&bs) 不是同一个? - RednaxelaFX 的回答(请留心阅读上面链接里关于虚方法表与JVM的部分。Sun的元祖JVM也是用虚方法表的喔。)

元祖JVM在做类加载的时候会把Class文件的各个部分分别解析(parse)为JVM的内部数据结构。例如说类的元数据记录在Class结构体里,每个方法的元数据记录在各自的methodblock结构体里,等等。

在刚加载好一个类的时候,Class文件里的常量池和每个方法的字节码(Code属性)会被基本原样的拷贝到内存里先放着,也就是说仍然处于使用“符号引用”的状态;直到真的要被使用到的时候才会被解析(resolve)为直接引用。

假定我们要第一次执行到foo()方法里调用bar()方法的那条invokevirtual指令了。此时JVM会发现该指令尚未被解析(resolve),所以会先去解析一下。通过其操作数所记录的常量池下标0x0002,找到常量池项#2,发现该常量池项也尚未被解析(resolve),于是进一步去解析一下。

通过Methodref所记录的class_index找到类名,进一步找到被调用方法的类的ClassClass结构体;然后通过name_and_type_index找到方法名和方法描述符,到ClassClass结构体上记录的方法列表里找到匹配的那个methodblock;最终把找到的methodblock的指针写回到常量池项#2里。

也就是说,原本常量池项#2在类加载后的运行时常量池里的内容跟Class文件里的一致,是:

[00 03] [00 11]

(tag被放到了别的地方;小细节:刚加载进来的时候数据仍然是按高位在前字节序存储的)而在解析后,假设找到的methodblock*是0x45762300,那么常量池项#2的内容会变为:

[00 23 76 45]

(解析后字节序使用x86原生使用的低位在前字节序(little-endian),为了后续使用方便)这样,以后再查询到常量池项#2时,里面就不再是一个符号引用,而是一个能直接找到Java方法元数据的methodblock*了。这里的methodblock*就是一个“直接引用”。

解析好常量池项#2之后回到invokevirtual指令的解析。回顾一下,在解析前那条指令的内容是:

[B6] [00 02]

而在解析后,这块代码被改写为:

[D6] [06] [01]

其中opcode部分从invokevirtual改写为invokevirtual_quick,以表示该指令已经解析完毕。原本存储操作数的2字节空间现在分别存了2个1字节信息,第一个是虚方法表的下标(vtable index),第二个是方法的参数个数。这两项信息都由前面解析常量池项#2得到的methodblock*读取而来。

也就是:

invokevirtual_quick vtable_index=6, args_size=1

这里例子里,类X对应在JVM里的虚方法表会是这个样子的:

[0]: java.lang.Object.hashCode:()I

[1]: java.lang.Object.equals:(Ljava/lang/Object;)Z

[2]: java.lang.Object.clone:()Ljava/lang/Object;

[3]: java.lang.Object.toString:()Ljava/lang/String;

[4]: java.lang.Object.finalize:()V

[5]: X.foo:()V

[6]: X.bar:()V

所以JVM在执行invokevirtual_quick要调用X.bar()时,只要顺着对象引用查找到虚方法表,然后从中取出第6项的methodblock*,就可以找到实际应该调用的目标然后调用过去了。

假如类X还有子类Y,并且Y覆写了bar()方法,那么类Y的虚方法表就会像这样:

[0]: java.lang.Object.hashCode:()I

[1]: java.lang.Object.equals:(Ljava/lang/Object;)Z

[2]: java.lang.Object.clone:()Ljava/lang/Object;

[3]: java.lang.Object.toString:()Ljava/lang/String;

[4]: java.lang.Object.finalize:()V

[5]: X.foo:()V

[6]: Y.bar:()V

于是通过vtable_index=6就可以找到类Y所实现的bar()方法。

所以说在解析/改写后的invokevirtual_quick指令里,虚方法表下标(vtable index)也是一个“直接引用”的表现。

在现在的HotSpot VM里,围绕常量池、invokevirtual的解析(再次强调是resolve)的具体实现方式跟元祖JVM不一样,但是大体的思路还是相通的。

HotSpot VM的运行时常量池有ConstantPool和ConstantPoolCache两部分,有些类型的常量池项会直接在ConstantPool里解析,另一些会把解析的结果放到ConstantPoolCache里。以前发过一帖有简易的图解例子,可以参考:请问,jvm实现读取class文件常量池信息是怎样呢?

==================================================

由此可见,符号引用通常是设计字符串的——用文本形式来表示引用关系。

而直接引用是JVM(或其它运行时环境)所能直接使用的形式。它既可以表现为直接指针(如上面常量池项#2解析为methodblock*),也可能是其它形式(例如invokevirtual_quick指令里的vtable index)。

关键点不在于形式是否为“直接指针”,而是在于JVM是否能“直接使用”这种形式的数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值