当通过 invokevirtual 调用方法时,JVM 如何知道要将多少个值弹出到一个新框架中?

How does the JVM know how many values to pop into a new frame when invoking a method via invokevirtual?

当通过 invokevirtual 调用方法时,调用方法会弹出要传递给被调用方法的值以及 objectref,并将它们放入新的堆栈帧中。

它如何知道哪个堆栈条目是 objectref?我的猜测是,它是通过查看被调用方法的类型并对其进行解析以确定要弹出多少值来实现的,但这似乎效率极低。我是否忽略了其他一些机制?

没有一种“正确”的方法可以做到这一点,但最简单的策略是将值留在堆栈中,被调用的方法通过负偏移量引用它们。例如,如果被调用的方法有 3 个参数,则从基础堆栈偏移量负 3、2 和 1 开始引用它们。每个都被复制到一个局部变量,然后以通常的方式引用。可以更新堆栈偏移以反映参数已被消耗。当然,每个local param也可以由一堆pop初始赋值,每个param一个。

可以执行其他技巧来加快速度。没有理由需要以不同于堆栈的方式存储局部变量。它们可以存储在堆栈本身中。传入的params占据了它们在栈中的原始位置,然后通过更新栈偏移量为剩余的局部变量分配额外的space。记住基本堆栈偏移量,并通过基本偏移量引用所有局部变量。

从本质上讲,局部变量就像一个堆栈槽,只是它可以随时访问,而不管当前压在顶部的是什么。

当您使用 class 文件格式作为起点时,method descriptor 是确定操作数堆栈中的哪些值必须成为新堆栈的第一个局部变量的唯一方法帧.

作为规则的例外,invokeinterface instruction has an embedded count which can be used to determine the number of (type 1) elements to consume. As the documentation 声明:

The count operand of the invokeinterface instruction records a measure of the number of argument values, where an argument value of type long or type double contributes two units to the count value and an argument of any other type contributes one unit. This information can also be derived from the descriptor of the selected method. The redundancy is historical.

这种历史冗余并没有改变 JVM 必须处理方法描述符作为此信息来源的事实,例如对于 invokevirtualinvokestaticinvokespecialinvokedynamic。此外,符合 JVM is required to verify this information,如果 invokeinterface 的计数与从方法描述符派生的计数不同,则抛出错误。

通常,验证器负责检测方法调用何时与堆栈帧的状态不一致,因此必须处理方法描述符并模拟它们对操作数堆栈的影响。这意味着,除非您使用的 JVM 在实际执行之前验证每条指令,否则即使不执行实际调用,它也必须处理这些描述符。显而易见的解决方案是在第一步中将方法描述符转换为更易于处理的内部表示。

简而言之,这些方法描述符 低效的,但通过合理的 JVM 实现,您只需支付一次费用,而不是每次调用都支付费用。