bboyjing's blog

自己动手写JVM二十三【方法调用和返回(一)】

本章开始将实现方法调用和返回,在此基础上,还会讨论类和对象的初始化。

方法调用概述

从调用角度来看,方法可以分为两类:静态方法和实例方法。静态方法通过类来调用,实例方法则通过对象引用来调用。静态方法时静态绑定的,也就是说,最终调用的时哪个方法在编译期就已经确定了。实例方法则支持动态绑定,最终要调用哪个方法可能要推迟到运行期才知道。
在Java7之前,Java虚拟机规范一共提供了4条方法调用指令。其中invokestatic指令来调用静态方法。invokespecial指令用来调用无需动态绑定的实例方法,包括构造函数、私有方法和通过supper关键字调用超类方法。剩下的情况则属于动态绑定。如果是针对接口类型的引用调用方法,就使用invokeinterfase指令,否则使用invokevirtual指令。本章将实现这四条指令,为了更好地支持动态类语言,Java7增加了一条方法调用指令invokedynamic,本章不讨论该指令。在深入讨论各条方法调用指令的细节之前,先简单了解下Java虚拟机是如何调用方法的。

首先,方法调用指令需要n+1个操作数,其中第一个操作数时uint16索引,在字节码中紧跟在指令操作码的后面。通过这个索引,可以从当前类的运行时常量池中知道到一个方法符号引用,解析这个符号引用就可以得到一个方法。注意,这个方法并不一定就是最终要调用的方法,所以可能还需要一个查找过程才能找到最终要调用的方法。剩下的n个操作数时要传递给被调用方法的参数,从操作数栈中弹出。如果要执行的是Java方法,下一步是给这个方法创建一个新的帧,并把它推到Java虚拟机栈顶。传递参数之后,新的方法就可以开始执行了。方法的最后一条指令时某个返回指令,这个指令负责把方法的返回值推入前一帧的操作数栈顶,然后把当前帧从Java虚拟机栈中弹出。

解析方法符号引用

非接口方法符号引用和接口方法符号引用的解析规则是不同的,因此下面分开讨论这两种符号引用。

非接口方法符号引用

修改/rtdata/heap/cp_methodref.go文件,实现ResolveMethod()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 解析非接口方法符号引用
func (self *MethodRef) ResolvedMethod() *Method {
if self.method == nil {
self.resolveMethodRef()
}
return self.method
}
func (self *MethodRef) resolveMethodRef() {
// 获取当前Class指针
d := self.cp.class
// 解析方法符号引用之前需要先解析方法所属的类
c := self.ResolvedClass()
// 判断方法所属类是否是接口
if c.IsInterface() {
panic("java.lang.IncompatibleClassChangeError")
}
// 根据方法名和描述符查找方法
method := lookupMethod(c, self.name, self.descriptor)
if method == nil {
panic("java.lang.NoSuchMethodError")
}
// 判断方法方法权限
if !method.isAccessibleTo(d) {
panic("java.lang.IllegalAccessError")
}
self.method = method
}

LookupMethodInClass()和lookupMethodInInterfaces()单独定义在/rtdata/heap/method_loopup.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func LookupMethodInClass(class *Class, name, descriptor string) *Method {
// 通过名称和描述符,从class以及superClass链中寻找方法
for c := class; c != nil; c = c.superClass {
for _, method := range c.methods {
if method.name == name && method.descriptor == descriptor {
return method
}
}
}
return nil
}
func lookupMethodInInterfaces(ifaces []*Class, name, descriptor string) *Method {
for _, iface := range ifaces {
// 先遍历当前接口的方法
for _, method := range iface.methods {
if method.name == name && method.descriptor == descriptor {
return method
}
}
// 如果没找到,再递归遍历当前接口实现的接口
method := lookupMethodInInterfaces(iface.interfaces, name, descriptor)
if method != nil {
return method
}
}
return nil
}

接口方法符号引用

修改/rtdata/heap/cp_interface_methodref.go,实现ResolveInterfaceMethod()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 解析接口方法符号引用
func (self *InterfaceMethodRef) ResolvedInterfaceMethod() *Method {
if self.method == nil {
self.resolveInterfaceMethodRef()
}
return self.method
}
func (self *InterfaceMethodRef) resolveInterfaceMethodRef() {
// 获取当前Class指针
d := self.cp.class
// 解析方法符号引用之前需要先解析方法所属的类
c := self.ResolvedClass()
// 判断方法所属类是否是接口
if !c.IsInterface() {
panic("java.lang.IncompatibleClassChangeError")
}
// 根据方法名和描述符查找方法
method := lookupInterfaceMethod(c, self.name, self.descriptor)
if method == nil {
panic("java.lang.NoSuchMethodError")
}
// 判断方法方法权限
if !method.isAccessibleTo(d) {
panic("java.lang.IllegalAccessError")
}
self.method = method
}
func lookupInterfaceMethod(iface *Class, name, descriptor string) *Method {
// 先从当前接口中查找方法
for _, method := range iface.methods {
if method.name == name && method.descriptor == descriptor {
return method
}
}
// 若没有找到,则递归当前接口实现的接口链
return lookupMethodInInterfaces(iface.interfaces, name, descriptor)
}

方法调用和参数传递

在定位到需要调用的方法之后,Java虚拟机要给这个方法创建一个新的帧并把它推入Java虚拟机栈顶,然后传递参数。这个逻辑对于本章要实现的4条方法调用指令来说基本上相同,为了避免重复代码,在单独的文件中实现这个逻辑。在/instructions/base包下创建method_invoke_logic.go文件,在其中实现InvokeMethod()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func InvokeMethod(invokerFrame *rtdata.Frame, method *heap.Method) {
// 获取当前线程
thread := invokerFrame.Thread()
// 创建新的帧
newFrame := thread.NewFrame(method)
// 将新创建的帧推入Java虚拟机栈
thread.PushFrame(newFrame)
argSlotSlot := int(method.ArgSlotCount())
if argSlotSlot > 0 {
for i := argSlotSlot - 1; i >= 0; i-- {
slot := invokerFrame.OperandStack().PopSlot()
newFrame.LocalVars().SetSlot(uint(i), slot)
}
}
}

重点讨论下参数的传递过程,从argSlotSlot开始,ArgSlotCount()函数确定方法的参数在局部变量表中占用多少位置。要注意的是,这个数量并不一定等于从Java代码中看到的参数个数,原因有两个:第一,long和duble类型的参数要占用两个位置;第二,对于实例方法,Java编译器会在参数列表的前面添加一个参数,这个隐藏的参数就是this引用。假设实际的参数占据n个位置,依次把这n个变量从调用者的操作数栈中弹出,放进被调用方法的局部变量表中,参数传递就完成了。
下面先在LocalVars结构体中新增setSlot()函数,修改/rtdata/local_vars.go文件。代码中没有对long和double类型做特殊处理,因为操作的是Slot结构体,所以没问题。

1
2
3
func (self LocalVars) SetSlot(index uint, slot Slot) {
self[index] = slot
}

下面忽略long和double类型参数,分别看一下静态方法和实例方法的参数传递的过程:
jvmgo_43
那么ArgSlotCount()方法到底返回什么呢,我们直接看代码,修改/rtdata/heap/method.go文件,修改Method结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type Method struct {
...
// 方法的参数所占的Slot数量
argSlotCount uint
}
func newMethods(class *Class, cfMethods []*classfile.MemberInfo) []*Method {
...
methods[i].calcArgSlotCount()
...
}
// 计算方法参数占用的slot数量
func (self *Method) calcArgSlotCount() {
// 分解方法描述符
parsedDescriptor := parseMethodDescriptor(self.descriptor)
for _, paramType := range parsedDescriptor.parameterTypes {
self.argSlotCount++
// long和double占两个slot
if paramType == "J" || paramType == "D" {
self.argSlotCount++
}
}
if !self.IsStatic() {
// 如果不是静态方法,给隐藏的参数this添加一个slot位置
self.argSlotCount++
}
}
// 获取方法的参数所占的Slot数量
func (self *Method) ArgSlotCount() uint {
return self.argSlotCount
}

其中parameterTypes定义在/rtdata/heap/method_descriptor.go文件中,parseMethodDescriptor()方法位于/rtdata/heap/method_descriptor_parser.go文件中。这块代码我也没细看,就不解释了,有兴趣的话自行到项目里看。

下节继续。。。