自己动手写 Java 虚拟机笔记 - 第七部分:方法调用与返回机制

自己动手写 Java 虚拟机笔记 - 第七部分:方法调用与返回机制

Luca Ju
2025-06-30 / 0 评论 / 4 阅读 / 正在检测是否收录...

前言

在前一章中,我们实现了类与对象的核心机制,包括类信息存储、对象创建和字段访问等。本章将聚焦 JVM 的方法调用与返回机制—— 这是实现函数执行、参数传递和结果返回的核心逻辑,涉及方法符号引用解析、调用指令执行、栈帧管理和类初始化等关键流程,是 JVM 支持面向对象编程的重要基础。

参考资料

《自己动手写 Java 虚拟机》—— 张秀宏

开发环境

工具 / 环境版本说明
操作系统MacOS 15.5基于 Intel/Apple Silicon 均可
JDK1.8用于字节码分析和测试
Go 语言1.23.10项目开发主语言

第七章:方法调用与返回机制实现

方法调用是程序执行的核心动作,JVM 通过特定指令(如 invoke_staticinvoke_virtual 等)实现不同类型方法的调用,并通过返回指令(如 ireturnlreturn 等)完成结果传递。本章将详细实现这些机制,包括符号引用解析、参数传递、栈帧管理和类初始化触发等逻辑。

一、方法调用概述

JVM 中的方法按调用方式可分为静态方法、实例方法、抽象方法等,不同类型的方法通过不同的指令调用。理解方法调用的核心概念和指令分类是实现的基础。

1. 方法类型与调用指令

JVM 提供的方法调用指令及其适用场景如下:

指令名称适用场景特点
invoke_static调用静态方法编译期确定目标方法,直接通过类名调用
invoke_special调用构造器、super 方法、private 方法编译期确定目标方法,不涉及动态绑定
invoke_virtual调用非静态方法(除 private 和构造器外)运行期通过对象实际类型查找方法(动态绑定,支持多态)
invoke_interface调用接口方法运行期查找实现接口的具体方法,比 invoke_virtual 多一步接口适配
invoke_dynamic动态调用(如 Lambda 表达式、方法引用)JDK 8 新增,支持动态语言特性(本章暂不实现)

核心区别:静态方法和 private 方法通过 “静态绑定” 在编译期确定目标方法;实例方法通过 “动态绑定” 在运行期根据对象实际类型查找方法,这是多态特性的核心实现。

2. 方法调用的核心流程

无论哪种调用指令,方法调用的基本流程一致,可概括为:

  1. 解析常量池中的方法符号引用,获取目标方法的直接引用;
  2. 从操作数栈弹出方法参数(按调用约定顺序);
  3. 创建目标方法的栈帧并压入虚拟机栈;
  4. 将弹出的参数放入新栈帧的局部变量表(参数传递);
  5. 执行目标方法的字节码指令;
  6. 方法执行完毕后,通过返回指令弹出当前栈帧,将返回值推入调用方栈帧的操作数栈。

二、方法符号引用解析

方法调用的第一步是将常量池中的 “方法符号引用”(编译期的间接引用)解析为 “直接引用”(运行期的内存地址),这是后续调用的基础。

1. 非接口方法的解析流程

MethodRef(类方法符号引用)为例,解析逻辑如下:

// ResolveMethod 将方法符号引用解析为直接引用(Method 实例)
func (r *MethodRef) ResolveMethod() *Method {
    if r.method == nil {
        r.resolveMethodRef() // 未解析则执行解析逻辑
    }
    return r.method
}

// resolveMethodRef 实际执行方法解析
func (r *MethodRef) resolveMethodRef() {
    currentClass := r.cp.class       // 引用所在的当前类
    methodClass := r.ResolveClass()  // 解析方法所属的类(符号引用→直接引用)

    // 校验:方法所属类不能是接口(接口方法需用 InterfaceMethodref)
    if methodClass.IsInterface() {
        panic("java.lang.IncompatibleClassChangeError")
    }

    // 查找目标方法(在类、父类、接口中递归匹配)
    method := lookupMethod(methodClass, r.name, r.descriptor)
    if method == nil {
        panic("java.lang.NoSuchMethodError") // 方法未找到
    }

    // 校验访问权限(当前类是否有权限调用目标方法)
    if !method.isAccessibleTo(currentClass) {
        panic("java.lang.IllegalAccessError")
    }

    r.method = method // 保存解析结果(直接引用)
}

2. 方法查找逻辑

方法查找需在类本身、父类及实现的接口中递归匹配,确保找到正确的方法:

// lookupMethod 查找类或接口中的方法
func lookupMethod(class *Class, name string, descriptor string) *Method {
    // 1. 在当前类中查找
    method := LookupMethodInClass(class, name, descriptor)
    if method != nil {
        return method
    }
    // 2. 在接口中查找(递归检查所有实现的接口)
    return lookupMethodInInterface(class.interfaces, name, descriptor)
}

// LookupMethodInClass 在单个类中查找方法(按名称和描述符匹配)
func LookupMethodInClass(class *Class, name string, descriptor string) *Method {
    // 遍历类的所有方法,匹配名称和描述符
    for _, method := range class.methods {
        if method.name == name && method.descriptor == descriptor {
            return method
        }
    }
    // 类中未找到,递归查找父类
    if class.superClass != nil {
        return LookupMethodInClass(class.superClass, name, descriptor)
    }
    return nil
}

// lookupMethodInInterface 在接口中递归查找方法
func lookupMethodInInterface(ifaces []*Class, name string, descriptor string) *Method {
    for _, iface := range ifaces {
        // 遍历接口的所有方法
        for _, method := range iface.methods {
            if method.name == name && method.descriptor == descriptor {
                return method
            }
        }
        // 接口未找到,递归查找父接口
        method := lookupMethodInInterface(iface.interfaces, name, descriptor)
        if method != nil {
            return method
        }
    }
    return nil
}

关键逻辑:方法查找遵循 “类优先于接口”“子类优先于父类” 的原则,确保符合 Java 的方法继承和重写规则。

三、方法调用与参数传递

解析到目标方法后,需创建栈帧、传递参数并执行方法,这是方法调用的核心执行阶段。

1. 方法调用的核心实现

// InvokeMethod 执行方法调用的核心逻辑
func InvokeMethod(invokerFrame *rtda.Frame, method *heap.Method) {
    thread := invokerFrame.Thread() // 获取当前线程

    // 1. 创建目标方法的栈帧(根据方法的 maxLocals 和 maxStack 初始化)
    newFrame := thread.NewFrame(method)
    // 2. 将新栈帧压入虚拟机栈
    thread.PushFrame(newFrame)

    // 3. 参数传递:从调用方操作数栈弹出参数,放入新栈帧的局部变量表
    argSlotCount := int(method.ArgSlotCount()) // 获取参数占用的槽位总数
    if argSlotCount > 0 {
        // 从后往前弹出参数(栈是先进后出,参数顺序需保持一致)
        for i := argSlotCount - 1; i >= 0; i-- {
            slot := invokerFrame.OperandStack().PopSlot() // 弹出参数
            newFrame.LocalVars().SetSlot(uint(i), slot)   // 存入局部变量表
        }
    }

    // 特殊处理:跳过 Native 方法(本章暂不实现 Native 方法逻辑)
    if method.IsNative() {
        if method.Name() == "registerNatives" {
            // 忽略 Object 类的 registerNatives 方法(无实际逻辑)
            thread.PopFrame()
        } else {
            panic(fmt.Sprintf("未实现 Native 方法:%v.%v%v", 
                method.Class().Name(), method.Name(), method.Descriptor()))
        }
    }
}

参数传递细节

  • 方法的参数数量和类型通过描述符(如 (ILjava/lang/String;)V 表示 2 个参数)确定,ArgSlotCount() 计算参数占用的槽位总数(longdouble 占 2 个槽位);
  • 由于操作数栈是 “先进后出” 结构,参数需从后往前弹出,才能按正确顺序存入局部变量表(索引 0 对应第一个参数)。

四、返回指令:结果传递与栈帧管理

方法执行完毕后,通过返回指令将结果传递给调用方,并弹出当前栈帧,恢复调用方的执行。

1. 返回指令的实现(以 ireturn 为例)

ireturn 用于返回 int 类型结果,其他类型(如 longfloat)的返回指令逻辑类似:

// IRETURN 返回 int 类型结果
type IRETURN struct {
    base.NoOperandsInstruction
}

func (i *IRETURN) Execute(frame *rtda.Frame) {
    thread := frame.Thread()
    // 1. 弹出当前方法的栈帧
    currentFrame := thread.PopFrame()
    // 2. 从当前栈帧的操作数栈弹出返回值
    result := currentFrame.OperandStack().PopInt()
    // 3. 将返回值推入调用方栈帧的操作数栈
    invokerFrame := thread.TopFrame()
    invokerFrame.OperandStack().PushInt(result)
}

2. 不同类型返回指令的共性

所有返回指令的核心流程一致,差异仅在于返回值的类型处理:

  • lreturn:返回 long 类型,弹出 8 字节值并推入调用方栈;
  • freturn/dreturn:返回 float/double 类型,通过浮点转码处理;
  • areturn:返回引用类型,弹出对象引用并推入调用方栈;
  • return:无返回值(void),仅弹出当前栈帧。

五、核心方法调用指令实现

不同的方法调用指令对应不同的解析和执行逻辑,以下以 invoke_virtual(动态绑定核心指令)为例说明。

1. invoke_virtual 指令:支持多态的实例方法调用

// INVOKE_VIRTUAL 调用实例方法(动态绑定)
type INVOKE_VIRTUAL struct {
    base.Index16Instruction // 包含常量池索引(指向方法符号引用)
}

func (i *INVOKE_VIRTUAL) Execute(frame *rtda.Frame) {
    currentClass := frame.Method().Class()
    cp := currentClass.ConstantPool()
    // 1. 解析方法符号引用
    methodRef := cp.GetConstant(i.Index).(*heap.MethodRef)
    resolvedMethod := methodRef.ResolveMethod()

    // 校验:不能调用静态方法
    if resolvedMethod.IsStatic() {
        panic("java.lang.IncompatibleClassChangeError")
    }

    // 2. 获取操作数栈中的对象引用(this)
    // 从栈顶弹出参数后,剩余的第一个元素为 this 引用
    ref := frame.OperandStack().GetRefFromTop(resolvedMethod.ArgSlotCount() - 1)
    if ref == nil {
        // 特殊处理:支持 System.out.println 等常用方法(简化实现)
        if methodRef.Name() == "println" {
            _println(frame.OperandStack(), methodRef.Descriptor())
            return
        }
        panic("java.lang.NullPointerException") // 对象为 null 时调用方法
    }

    // 3. 权限校验(protected 方法的访问控制)
    if resolvedMethod.IsProtected() && 
        resolvedMethod.Class().IsSuperClassOf(currentClass) &&
        resolvedMethod.Class().GetPackageName() != currentClass.GetPackageName() &&
        ref.Class() != currentClass && !ref.Class().IsSubClassOf(currentClass) {
        panic("java.lang.IllegalAccessError")
    }

    // 4. 动态绑定:根据对象实际类型查找方法(而非编译期类型)
    methodToBeInvoked := heap.LookupMethodInClass(ref.Class(), methodRef.Name(), methodRef.Descriptor())
    if methodToBeInvoked == nil || methodToBeInvoked.IsAbstract() {
        panic("java.lang.AbstractMethodError") // 方法未实现
    }

    // 5. 执行方法调用
    base.InvokeMethod(frame, methodToBeInvoked)
}

动态绑定核心invoke_virtual 不直接使用符号引用解析的方法,而是根据对象的实际类型(ref.Class())重新查找方法,这确保了运行时调用的是子类重写的方法,实现多态特性。

六、改进解释器:支持日志与多方法执行

为便于调试和跟踪方法调用流程,改进解释器以支持指令日志输出,并完善多方法连续执行的逻辑。

1. 解释器主循环优化

// loop 解释器主循环,支持指令日志输出
func loop(thread *rtda.Thread, logInst bool) {
    reader := &base.BytecodeReader{}
    for {
        frame := thread.CurrentFrame() // 获取当前栈顶栈帧
        pc := frame.NextPC()          // 获取程序计数器
        thread.SetPC(pc)

        // 解析指令
        reader.Reset(frame.Method().Code(), pc)
        opcode := reader.ReadInt8()                // 读取 opcode
        inst := instructions.NewInstruction(byte(opcode)) // 创建指令实例
        inst.FetchOperands(reader)                 // 读取操作数

        // 输出指令日志(若启用)
        if logInst {
            logInstruction(frame, inst)
        }

        // 执行指令并更新程序计数器
        frame.SetNextPC(reader.PC())
        inst.Execute(frame)

        // 线程栈为空时结束循环(所有方法执行完毕)
        if thread.IsStackEmpty() {
            break
        }
    }
}

日志功能:通过 verbose:inst 参数启用指令日志,可输出当前执行的指令、PC 地址等信息,便于跟踪方法调用流程和调试问题。

七、类初始化:触发与执行 <clinit> 方法

类在首次被使用时需执行初始化(执行类构造器 <clinit> 方法),方法调用是触发初始化的重要场景之一。

1. 类初始化的触发条件

以下情况会触发类初始化(执行 <clinit> 方法):

  • 执行 new 指令创建对象时,类未初始化;
  • 执行 putstatic/getstatic 指令访问静态字段时,类未初始化;
  • 执行 invoke_static 指令调用静态方法时,类未初始化;
  • 初始化子类时,父类未初始化;
  • 反射操作访问类时,类未初始化。

2. 类初始化逻辑实现

// InitClass 执行类初始化(触发 <clinit> 方法)
func InitClass(thread *rtda.Thread, class *heap.Class) {
    class.StartInit()          // 标记类开始初始化(防止重复初始化)
    scheduleClinit(thread, class) // 执行类构造器 <clinit> 方法
    initSuperClass(thread, class) // 递归初始化父类
}

// scheduleClinit 计划执行 <clinit> 方法
func scheduleClinit(thread *rtda.Thread, class *heap.Class) {
    clinit := class.GetClinitMethod() // 获取 <clinit> 方法
    if clinit != nil {
        // 创建 <clinit> 方法的栈帧并压入栈
        frame := thread.NewFrame(clinit)
        thread.PushFrame(frame)
    }
}

// initSuperClass 初始化父类(确保父类先于子类初始化)
func initSuperClass(thread *rtda.Thread, class *heap.Class) {
    if !class.IsInterface() {
        superClass := class.SuperClass()
        if superClass != nil && !superClass.InitStarted() {
            InitClass(thread, superClass) // 递归初始化父类
        }
    }
}

核心逻辑:类初始化通过执行 <clinit> 方法完成,该方法由编译器自动生成,包含静态变量初始化和静态代码块逻辑。初始化过程中会确保父类先于子类初始化,符合 Java 的类加载规范。

本章小结

本章实现了 JVM 方法调用与返回的核心机制,重点包括:

  1. 方法调用指令分类:区分 invoke_staticinvoke_virtual 等指令的适用场景,支持静态绑定和动态绑定;
  2. 符号引用解析:通过递归查找类、父类和接口,将方法符号引用解析为直接引用;
  3. 参数传递与栈帧管理:创建栈帧并通过操作数栈传递参数,确保方法调用的上下文正确;
  4. 返回指令实现:通过不同类型的返回指令传递结果,弹出当前栈帧恢复调用方执行;
  5. 类初始化触发:在方法调用等场景中触发类初始化,执行 <clinit> 方法完成静态变量初始化。

这些机制共同支撑了 JVM 对面向对象特性(如多态、继承)的支持,是实现完整 Java 程序执行的关键基础。下一章将进一步完善异常处理和数组支持,增强 JVM 的功能完整性。

源码地址:https://github.com/Jucunqi/jvmgo.git
1

评论 (0)

取消