本系列文章基于 OpenJDK 9,聚焦 x86 平台。
本文以 invokeinterface
指令为例,分析 HotSpot JVM 如何解释执行 字节码指令 。
正文
前面文章展示了从操作数解析出 符号引用 再解析出 直接引用 的过程,解析出的信息存储在 常量池缓存 中。那么这些信息是怎么使用的呢?
直接上代码,因为代码较长,我们分段解释。
源码文件(openjdk\hotspot\src\share\vm\interpreter\bytecodeInterpreter.cpp)。
获取常量池缓存条目
1 | CASE(_invokeinterface): { |
字节码指令 后面,紧跟两个字节的操作数。如果已经解析过,操作数即为 常量池缓存条目 索引。
代码中,首先取出操作数,然后尝试籍此获得 常量池缓存条目。
根据条目信息,可以知道是否已经解析过。如果没有解析过,就进行前面文章展示的解析流程。
解析完成,再获取 常量池缓存条目,此时一定存在该条目。
若强制virtual调用
1 | // Special case of invokeinterface called for virtual method of |
有些情况下,虽然 字节码指令 是 invokeinterface
,但会强制走 invokevirtual
。
如果指令执行的方法是 final
的, 那么 常量池缓存条目 的 _f2
字段存储的就是方法的 直接引用。
否则,先从栈里获取到调用方法的对象实例 rcvr
,再籍此获取到位于 元空间 的 类元数据 rcvrKlass
。此时,常量池缓存条目 的 _f2
字段存储的是 虚表索引,有了索引,就可以从 类元数据 中的 虚表 中获取到方法的 直接引用。
调用即可。
正常interface调用
1 | // this could definitely be cleaned up QQQ |
常量池缓存条目 的 _f1
字段存储的是 接口元数据,_f2
字段存储是 接口表索引。
从栈里获取到调用方法的对象实例 rcvr
,再籍此获取到位于 元空间 的 类元数据 rcvr->klass()
。
因为一个类只会继承自一个基类,却可以实现多个接口,所以 接口表 结构比 虚表 复杂。大体是,分别有 1 个 接口偏移表 和 N 个 接口表。接口偏移表 的条目存储了 接口元数据 和对应 接口表 的偏移地址。通过跟前面说的 _f1
字段存储的值进行比对,就能找到方法所在的那个接口,获取到其 接口表 的偏移地址。
有了 类元数据 和 接口表 的偏移地址,就能找到对应的 接口表。再根据 _f2
字段存储的 接口表索引,就能找到方法的 直接引用。
调用即可。
这样,该 字节码指令 解释执行即对应的方法调用就完成了。
话外
invokeinterface
比 invokevirtual
更复杂些,所以本文以此为例。
为了便于理解,选择了 CPP解释器 的执行过程,模板解释器 过程是类似的,只是生成了机器码供运行时执行。
结合前面两篇文章,这就是 invoke
类型的 字节码指令 解析引用并解释执行的主要过程。