美文网首页
[Golang实现JVM第五篇]静态方法调用的实现

[Golang实现JVM第五篇]静态方法调用的实现

作者: 司青玄 | 来源:发表于2020-07-08 15:50 被阅读0次

    一直以来又长又臭的调用链简直就是Java语言的标志性特色,方法调用可谓是Java世界里表达一切逻辑的基石。现在我们终于具备了实现它的基础。

    JVM中的5条方法调用指令

    在JVM中触发方法调用的指令有5条,分别是:

    • invokestatic

    调用静态方法

    • invokespecial

    调用构造方法

    • invokeinterface

    调用接口方法

    • invokevirtual

    调用对象方法

    • invokedynamic

    jdk1.7中引入,给动态语言预留的调用指令。指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 1.7新加入的CONSTANT_InvokeDynamic_info常量

    <br />

    <br />

    invokestatic指令的实现

    这里面最简单的就是invokestatic静态方法调用指令了,因为静态方法不需要创建对象,属于类。这条指令后面紧跟着两个字节,表示常量池中方法引用常量的下标:

    invokestatic byte1 byte2
    

    这样的话我们就可以从常量池中找到CONSTANT_Methodref_info常量,结构如下:

    // 方法引用常量
    type MethodRefConstInfo struct {
        Tag uint8
        ClassIndex uint16
        NameAndTypeIndex uint16
    }
    

    可以看到,里面记录了方法所在的类,和一个NameAndType常量的索引。其中NameAndType的结构如下:

    type NameAndTypeConst struct {
        Tag uint8
        NameIndex uint16
        DescIndex uint16
    }
    

    其中NameIndex又是一个下标,指向的是常量池中的UTF8属性,表示方法的简单名,例如sayHello

    DescIndex也是一个下标,指向的是常量池中的UTF8属性,表示方法描述符,例如(ILjava/lang/String;)Ljava/lang/String;。方法描述符描述了一个方法的参数类型、数量和返回类型,其中括号里的(ILjava/lang/String;)表示的是方法参数,I表示基本类型intLjava/lang/String;表示对象类型。注意基本类型跟其他类型之间是没有任何分隔符的,如果是对象类型,则以大写字母L开头,然后紧跟着类的全名,最后以;结束。括号外的Ljava/lang/String;表示此方法的返回类型也是String,如果没有返回值则表示为V,没有参数的话则表示为一对空括号()。综上所述,描述符(ILjava/lang/String;)Ljava/lang/String;表示第一个参数为int类型,第二个参数为String类型,返回类型为String的方法:

    String foo(int, String) {}
    

    有了方法名、方法签名、类名,我们就可以唯一确定一个方法了。在调用之前还要注意参数顺序问题,调用前javac会生成一系列参数压栈的指令,但是我们在取参数出栈的时候,由于栈先进先出的性质,弹出参数的顺序跟实际顺序是相反的,这一点一定要小心。

    最后我们再来看一下常量池中的UTF8数据项结构:

    type Utf8InfoConst struct {
        Tag uint8
        Length uint16
        Bytes []byte
    }
    

    里面的Bytes就是UTF8字节流了,可以直接转换成string。class里所有对字符串的记录都是通过UTF8属性保存的,想引用这个字符串的话就用UTF8属性在常量池的下标来引用。

    <br />

    <br />

    有了上面的分析,我们就可以按图索骥的去实现了。首先,我们要把指令后面的byte1 byte2组装回整数:

    methodRefCpIndex := (indexbyte1 << 8) | indexbyte2
    

    然后取出方法引用常量、取出方法名、方法描述符、方法所在的class全名:

    / 取出引用的方法
        methodRef := def.ConstPool[methodRefCpIndex].(*class.MethodRefConstInfo)
        // 取出方法名
        nameAndType := def.ConstPool[methodRef.NameAndTypeIndex].(*class.NameAndTypeConst)
        methodName := def.ConstPool[nameAndType.NameIndex].(*class.Utf8InfoConst).String()
        // 描述符
        descriptor := def.ConstPool[nameAndType.DescIndex].(*class.Utf8InfoConst).String()
        // 取出方法所在的class
        classRef := def.ConstPool[methodRef.ClassIndex].(*class.ClassInfoConstInfo)
        // 取出目标class全名
        targetClassFullName := def.ConstPool[classRef.FullClassNameIndex].(*class.Utf8InfoConst).String()
    

    加载class:

    // 加载
        targetDef, err := i.miniJvm.findDefClass(targetClassFullName)
        if nil != err {
            return fmt.Errorf("failed to load class for '%s': %w", targetClassFullName, err)
        }
    

    这里的findDefClass()方法会首先从已经加载的类中查找,如果没有,就会遍历classpath下的所有.class文件(包括jar包中的文件),找到全限定性名相符的class文件,执行class解析逻辑(第二篇中有讲),然后返回一个DefClass结构的指针。

    然后就可以直接调用方法了:

    // 调用
        return i.ExecuteWithFrame(targetDef, methodName, descriptor, frame)
    

    ExecuteWithFrame方法中实现了方法描述符的解析、方法栈帧的创建和参数的传递,代码比较复杂不再罗列了,可以在这里找到完整代码:https://github.com/wanghongfei/mini-jvm/blob/master/vm/interpreted_execution_engine.go

    <br />

    <br />

    至此我们就完成了JVM中最简单的方法调用指令的实现。为什么说最简单呢,因为invokestatic不需要创建对象,也不涉及复杂的方法查找逻辑,只需要匹配类名、方法名、方法签名就可以了。静态方法调用的实现是以后实现native方法的基石,在下一篇中就会介绍。

    相关文章

      网友评论

          本文标题:[Golang实现JVM第五篇]静态方法调用的实现

          本文链接:https://www.haomeiwen.com/subject/wsyccktx.html