手写JVM(十九)-方法调用

代码、内容参考来自于张秀宏大佬的自己动手写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)
}
分配内存有一个make函数,该函数第一个参数是类型,第二个参数是分配的空间,第三个参数是预留分配空间,例如a:=make([]int, 5, 10), len(a)输出结果是5,cap(a)输出结果是10。cap函数计算左指针到原array最后的值的个数。对a[5]进行赋值会报错,而append 所做的是在切片尾添加元素并返回结果。使用append则不会报错。

 

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核心技术系列)

GO语言官网:Standard library – Go Packages

Java虚拟机规范:Chapter 4. The class File Format (oracle.com)

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇