代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。
1.方法调用概述
从调用的角度来看,方法可以分为两类:静态方法(或者类方法)和实例方法。静态方法通过类来调用,实例方法则通过对象引用来调用。静态方法是静态绑定的,也就是说,最终调用的是哪个方法在编译期就已经确定。实例方法则支持动态绑定,最终要调用哪个方法可能要推迟到运行期才能知道,本章将详细讨论这一点。
从实现的角度来看,方法可以分为三类:没有实现(也就是抽象方法)、用Java语言(或者JVM上的其他语言,如Groovy和Scala等)实现和用本地语言(如C或者C++)实现。静态方法和抽象方法是互斥的。在Java 8之前,接口只能包含抽象方法。为了实现Lambda表达式,Java 8放宽了这一限制,在接口中也可以定义静态方法和默认方法。本章不考虑接口的静态方法和默认方法。
方法调用指令:
invokevirtual、 invokeinterface、 invokespecial、invokestatic、invokedynamic
以下5条指令用于方法调用:
- invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派) ,支持多态。这也是Java语言中最常见的方法分派方式。
- invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。
- invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。(注意,这三种方法不存在方法重写)这些方法都是静态类型绑定的,不会在调用时进行动态派发。
- invokestatic指令用于调用命名类中的类方法(static方法)。这是静态绑定的。
- invokedynamic:调用动态绑定的方法,这个是JDK 1.7后新加入的指令,用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
本章将实现invokevirtual、 invokeinterface、 invokespecial、invokestatic这4条指令。
Java虚拟机是如何调用方法的:
首先,方法调用指令需要n+1个操作数,其中第1个操作数是uint16索引,通过这个索引,可以从当前类的运行时常量池中找到一个方法符号引用,解析这个符号引用就可以得到一个方法。注意,这个方法并不一定就是最终要调用的那个方法,所以可能还需要一个查找过程才能找到最终要调用的方法。剩下的n个操作数是要传递给被调用方法的参数,从操作数栈中弹出。如果要执行的是Java方法(而非本地方法),下一步是给这个方法创建一个新的帧,并把它推到Java虚拟机栈顶。传递参数之后,新的方法就可以开始执行了。以上针对非本地方法,本地方法调用则推迟到第9章再讨论。
方法的最后一条指令是某个返回指令,这个指令负责把方法的返回值推入前一帧的操作数栈顶,然后把当前帧从Java虚拟机栈中弹出。
2.解析方法符号引用
非接口方法符号引用和接口方法符号引用的解析规则是不同的,Java虚拟机规范的5.4.3.3节和5.4.3.4节详细描述了这两种符号引用的解析规则。主要参考Java虚拟机规范第7版编写代码,因为我们不讨论接口的静态方法和默认方法。
非接口方法符号引用
打开rtda\heap\cp_methodref.go文件,在其中实现ResolvedMethod()方法,代码如下:
func (self *MethodRef) ResolvedMethod() *Method {
if self.method == nil {
self.resolveMethodRef()
}
return self.method
}如果还没有解析过符号引用,调用resolveMethodRef()方法进行解析,否则直接返回方法指针。resolveMethodRef()方法的代码如下:
// jvms8 5.4.3.3
func (self *MethodRef) resolveMethodRef() {
//如果类D想通过方法符号引用访问类C的某个方法,先要解析符号引用得到类C。
d := self.cp.class
c := self.ResolvedClass()
//如果C是接口,则抛出IncompatibleClassChangeError异常
if c.IsInterface() {
panic("java.lang.IncompatibleClassChangeError")
}
//否则根据方法名和描述符查找方法。
method := lookupMethod(c, self.name, self.descriptor)
//如果找不到对应的方法,则抛出NoSuchMethodError异常,
if method == nil {
panic("java.lang.NoSuchMethodError")
}
//否则检查类D是否有权限访问该方法。如果没有,则抛出IllegalAccessError异常。
if !method.isAccessibleTo(d) {
panic("java.lang.IllegalAccessError")
}
self.method = method
}
isAccessibleTo()方法是在ClassMember结构体中定义的,在第6章就已经实现了。

下面看一下lookupMethod()函数,先从C的继承层次中找,如果找不到,就去C的接口中找。其代码如下:
func lookupMethod(class *Class, name, descriptor string) *Method {
//先从C的继承层次中找。
method := LookupMethodInClass(class, name, descriptor)
if method == nil {
//如果找不到,就去C的接口中找
method = lookupMethodInInterfaces(class.interfaces, name, descriptor)
}
return method
}
LookupMethodInClass()函数,lookupMethodInInterfaces()函数在很多地方都要用到,所以新建ch07\rtda\heap\method_lookup.go文件,代码如下:
package heap
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
}接口方法符号引用
打开\rtda\heap\cp_interface_methodref.go文件,在其中实现ResolvedInterfaceMethod()方法,代码如下:
package heap
import "jvmgo/ch07/classfile"
type InterfaceMethodRef struct {
MemberRef
method *Method
}
func newInterfaceMethodRef(cp *ConstantPool, refInfo *classfile.ConstantInterfaceMethodrefInfo) *InterfaceMethodRef {
ref := &InterfaceMethodRef{}
ref.cp = cp
ref.copyMemberRefInfo(&refInfo.ConstantMemberrefInfo)
return ref
}
// 解析接口方法符号引用
func (self *InterfaceMethodRef) ResolvedInterfaceMethod() *Method {
if self.method == nil {
self.resolveInterfaceMethodRef()
}
return self.method
}
// jvms8 5.4.3.4
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
}
// todo
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)
}如果能在接口中找到方法,就返回找到的方法,否则调用lookupMethodInInterfaces()函数在超接口中寻找。
3.方法调用和参数传递
可以从当前类的运行时常量池中找到一个方法符号引用,解析这个符号引用就可以得到一个方法之后,Java虚拟机会给这个方法创建一个新的帧并把它推入Java虚拟机栈顶,然后传递参数。invokevirtual、 invokeinterface、 invokespecial、invokestatic这4条指令都有这个逻辑,所以在单独的文件中实现这个逻辑。
在\instructions\base目录下创建method_invoke_logic.go文件,在其中实现InvokeMethod()函数,代码如下:
package base
import (
"jvmgo/ch07/rtda"
"jvmgo/ch07/rtda/heap"
)
func InvokeMethod(invokerFrame *rtda.Frame, method *heap.Method) {
// 获取当前线程
thread := invokerFrame.Thread()
// 创建新的帧
newFrame := thread.NewFrame(method)
// 将新创建的帧推入Java虚拟机栈
thread.PushFrame(newFrame)
//确定方法的参数在局部变量表中占用多少位置
argSlotCount := int(method.ArgSlotCount())
if argSlotCount > 0 {
for i := argSlotCount - 1; i >= 0; i-- {
//操作数栈中弹出
slot := invokerFrame.OperandStack().PopSlot()
//放进被调用方法的局部变量表
newFrame.LocalVars().SetSlot(uint(i), slot)
}
}
}函数的前三行代码创建新的帧并推入Java虚拟机栈,剩下的代码传递参数。首先,要确定方法的参数在局部变量表中占用多少位置。其中,long和double类型的参数要占用两个位置。而对于实例方法,Java编译器会在参数列表的前面添加一个this引用。假设实际的参数占据n个位置,依次把这n个变量从调用者的操作数栈中弹出,放进被调用方法的局部变量表中,参数传递就完成了。
注意,在代码中,并没有对long和double类型做特别处理。因为操作的是Slot结构体,即直接将对long和double类型做特别处理后的Slot结构体数组LocalVars传入。
在/rtda/local_vars.go加上SetSlot()方法代码如下:
func (self LocalVars) SetSlot(index uint, slot Slot) {
self[index] = slot
}忽略long和double类型参数要占用两个位置,则静态方法的参数传递过程如图:

忽略long和double类型参数要占用两个位置,则实例方法的参数传递过程如图:

ArgSlotCount()方法确定方法的参数在局部变量表中占用多少位置,
打开\rtda\heap\method.go文件,修改Method结构体,给它添加argSlotCount字段
type Method struct {
ClassMember
maxStack uint
maxLocals uint
code []byte
argSlotCount uint
}
而ArgSlotCount()只是个Getter方法
func (self *Method) ArgSlotCount() uint {
return self.argSlotCount
}newMethods()方法也需要修改,调用calcArgSlotCount()方法计算方法的argSlotCount,代码如下:
func newMethods(class *Class, cfMethods []*classfile.MemberInfo) []*Method {
methods := make([]*Method, len(cfMethods))
for i, cfMethod := range cfMethods {
methods[i] = &Method{}
methods[i].class = class
methods[i].copyMemberInfo(cfMethod)
methods[i].copyAttributes(cfMethod)
methods[i].calcArgSlotCount()
}
return methods
}
calcArgSlotCount方法的代码:
// 计算方法参数占用的slot数量
func (self *Method) calcArgSlotCount() {
// 分解方法描述符成string数组
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++ // `this` reference
}
}
parseMethodDescriptor()函数分解方法描述符,返回一个MethodDescriptor结构体实例。这个结构体定义在ch06\rtda\heap\method_descriptor.go文件中,addParameterType函数在下面的method_descriptor_parser中用到,顺便加上,在代码如下:
package heap
type MethodDescriptor struct {
parameterTypes []string
returnType string
}
//其实就是扩容,并添加一个新的值到parameterTypes数组。
func (self *MethodDescriptor) addParameterType(t string) {
pLen := len(self.parameterTypes)
//cap函数计算左指针到原array最后的值的个数
if pLen == cap(self.parameterTypes) {
//预留4个长度
s := make([]string, pLen, pLen+4)
copy(s, self.parameterTypes)
self.parameterTypes = s
}
//在切片尾添加元素
self.parameterTypes = append(self.parameterTypes, t)
}
parseMethodDescriptor函数
因为,《自己动手写java虚拟机》没有对parseMethodDescriptor()函数解释,所以我就解释一下。
parseMethodDescriptor函数定义在ch06\rtda\heap\method_descriptor_parser.go文件中,
这个函数其实就是将描述符按照指定格式返回。
我们回顾一下描述符。
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte,char,double,float,int,long,short,boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象则用字符L加对象的全限定名来表示,如下所示:

比如java代码:
public void test(int x,String y,long z){
int s = 1;
}而在字节码我们读到的描述符:

先使用将参数放入()内,后接返回值,这里是V,也就是void
()内放的就是参数,直接将参数的字符放入。
所以这个函数其实就是将描述符按照MethodDescriptor结构体格式返回。
下面代码,我加了详细的解释,其实就是对描述符如(ILjava/lang/String;J)V。将其转换成string数组
package heap
import "strings"
type MethodDescriptorParser struct {
raw string
offset int
parsed *MethodDescriptor
}
func parseMethodDescriptor(descriptor string) *MethodDescriptor {
//新建一个指向MethodDescriptorParser结构体的指针
parser := &MethodDescriptorParser{}
//真正分解方法描述符的逻辑
return parser.parse(descriptor)
}
func (self *MethodDescriptorParser) parse(descriptor string) *MethodDescriptor {
//将方法描述符存入raw,方便操作
self.raw = descriptor
self.parsed = &MethodDescriptor{}
//判断参数的开始
self.startParams()
//真正参数逻辑
self.parseParamTypes()
//判断参数的结束
self.endParams()
//真正返回值逻辑
self.parseReturnType()
//返回值后描述符应该结束
self.finish()
return self.parsed
}
//参数的开启
func (self *MethodDescriptorParser) startParams() {
if self.readUint8() != '(' {
//不符方法描述符规范,终止程序
self.causePanic()
}
}
//参数的结束
func (self *MethodDescriptorParser) endParams() {
if self.readUint8() != ')' {
//不符方法描述符规范,终止程序
self.causePanic()
}
}
func (self *MethodDescriptorParser) finish() {
//返回值后描述符应该结束
if self.offset != len(self.raw) {
//不符方法描述符规范,终止程序
self.causePanic()
}
}
//终止程序的逻辑
func (self *MethodDescriptorParser) causePanic() {
panic("BAD descriptor: " + self.raw)
}
//逐个返回方法描述符
func (self *MethodDescriptorParser) readUint8() uint8 {
b := self.raw[self.offset]
self.offset++
return b
}
func (self *MethodDescriptorParser) unreadUint8() {
self.offset--
}
func (self *MethodDescriptorParser) parseParamTypes() {
for {
t := self.parseFieldType()
if t != "" {
//添加一个新的值到parameterTypes数组。
self.parsed.addParameterType(t)
} else {
break
}
}
}
//对返回值的操作
func (self *MethodDescriptorParser) parseReturnType() {
//void直接返回
if self.readUint8() == 'V' {
self.parsed.returnType = "V"
return
}
self.unreadUint8()
t := self.parseFieldType()
if t != "" {
self.parsed.returnType = t
return
}
self.causePanic()
}
func (self *MethodDescriptorParser) parseFieldType() string {
switch self.readUint8() {
case 'B':
return "B"
case 'C':
return "C"
case 'D':
return "D"
case 'F':
return "F"
case 'I':
return "I"
case 'J':
return "J"
case 'S':
return "S"
case 'Z':
return "Z"
case 'L':
//对象
return self.parseObjectType()
case '[':
//数组
return self.parseArrayType()
default:
self.unreadUint8()
return ""
}
}
func (self *MethodDescriptorParser) parseObjectType() string {
unread := self.raw[self.offset:]
//IndexRune返回unread的第一个";"的索引,如果不存在于,则返回-1。
semicolonIndex := strings.IndexRune(unread, ';')
if semicolonIndex == -1 {
self.causePanic()
return ""
} else {
//截取描述符中的对象的全类名
objStart := self.offset - 1
objEnd := self.offset + semicolonIndex + 1
self.offset = objEnd
descriptor := self.raw[objStart:objEnd]
return descriptor
}
}
func (self *MethodDescriptorParser) parseArrayType() string {
//截取描述符中的数组
arrStart := self.offset - 1
self.parseFieldType()
arrEnd := self.offset
descriptor := self.raw[arrStart:arrEnd]
return descriptor
}
4.参考
尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
周志明:深入理解java虚拟机
张秀宏:自己动手写Java虚拟机 (Java核心技术系列)


