[Golang实现JVM第七篇]实现invokevirtual和虚方法表
本篇我们专注invokevirtual
这一条指令,先通过简单粗暴的方式实现指令的功能,然后探究如何通过著名的虚方法表(Virtual Method Table)来进行一些优化。
指令含义
invokevirtual
用于调用除静态方法、构造方法、私有方法、接口方法外的所有方法。其指令的格式为:
invokevirtual = 182 (0xb6)
Format:
invokevirtual indexbyte1 indexbyte2
Operand Stack:
..., objectref, [arg1, [arg2 ...]] →
其中indexbyte1
和indexbyte2
为一个uint16类型的无符号整数,表示常量池中方法引用常量的下标,这点跟invokestatic
是完全相同的。后面的"→"表示出栈顺序,即执行到invokevirtual
时操作数栈的状态是方法参数在上,对象引用在下,其中方法参数是可选的,而且出栈时参数的顺序会跟方法定义里的顺序相反。
这里最重要的就是在objectref
中查找目标方法的过程。我们先来一段简单的代码:
public class ClassExtendTest {
public static void main(String[] args) {
Person person = new Person();
Printer.print(person.say());
person = new Student(); // Student继承了Person
Printer.print(person.say());
}
}
注意最后两行,Student
是Person
的子类,如果使用Person类型的变量保存其子类Student对象的引用,然后调用被子类重写了的say()
方法,这时候编译出的字节码如下:
15: new #6 // class com/fh/Student
18: dup
19: invokespecial #7 // Method com/fh/Student."<init>":()V
22: astore_1
23: aload_1
24: invokevirtual #4 // Method com/fh/Person.say:()I
可以看到,偏移量为15 ~ 19的字节码用于创建Student对象并调用构造方法,然后astore_1
则将刚创建的Student对象的引用保存到了本地变量表下标为1的槽位中,紧接着aload_1
就将本地变量表里的Student引用压入栈顶,这个就是前面JVM规范里提到的objectref
,同时也是say()
方法的真实接收者。这样当我们就可以从刚创建的Student对象中查找say()
方法了,而不是其父类Person。对于后面invokevirtual
跟着的两个bytes所指向的常量池里的方法常量
// 方法引用常量
type MethodRefConstInfo struct {
Tag uint8
ClassIndex uint16
NameAndTypeIndex uint16
}
我们只关心里面的方法名和方法描述符,即NameAndTypeIndex
字段,忽略ClassIndex
,因为栈顶已经有方法真实接收者的引用了。
综上,invokevirtual
指令查找方法的过程如下(省略访问权限验证):
- 从操作数栈顶开始向下遍历,找到
objectref
元素,取出(不弹出) - 取出
objectref
的class元数据,遍历方法组数,查找方法名跟方法描述符都匹配的方法,如果找到了就直接返回,没找到进入下一步 - 判断有没有父类,如果有则在父类中执行同样的查找,如果都没找到则抛出
NoSuchMethodException
异常
虚方法表
从上面的步骤可以看出,每次执行invokevirtual
指令都要在class元数据中做大量的查找,并且由于MethodRefConstInfo
里并没有直接保存方法名和描述符本身,而是保存了他们在常量池的索引,因此整个流程下来需要多次访问常量池才能获取到定位方法所需要的全部信息。对此我们可以使用虚方法表(Virtual Method Table)加以优化。
VTable本质上就是一个数组,数组的每个元素都保存了目标方法的入口和一些其他方便JVM具体实现所使用的方法信息。对于Object类,他的虚方法表里就会只保存Object里的里的公开方法;对于子类来说,方法表里的数据项排序一定是父类方法在前,自己新定义的方法在后,如果自己重写了父类的方法,那么只需要将前面的父类方法所在的数据项里的方法入口替换为子类自己的方法入口即可。
这里额外解释一下到底什么是"方法入口"。方法入口的说法是一种宏观且抽象的叫法,具体到代码里以什么方式体现是因不同的JVM实现而不同的。例如,如果你用C实现JVM,那么方法入口可以是一个方法指针,不过在我们Go实现的Mini-JVM中,方法入口则是包含了字节码的
MethodInfo
结构体指针,这里面保存了字节码数组因而可以直接遍历解释执行。
那么虚方法表如何提高方法查找效率呢?具体有两个层次的实现,一是在查找方法时直接遍历虚方法表而不是class元数据,这样就省去了多次访问常量池的开销,但是仍然需要一个个对比虚方法表里的数据项看看是不是要找的目标方法;二是在方法第一次执行时,我们可以将方法的符号引用直接替换为虚方法表数组的下标,这样以后再调用就能一步到找到目标方法,不需要任何遍历操作了。
Mini-JVM暂未实现对虚方法表第二个层次的优化,狗头
代码实现
以下片段摘自 https://github.com/wanghongfei/mini-jvm/blob/master/vm/interpreted_execution_engine.go ;
解析指令:
func (i *InterpretedExecutionEngine) invokeVirtual(def *class.DefFile, frame *MethodStackFrame, codeAttr *class.CodeAttr) error {
twoByteNum := codeAttr.Code[frame.pc + 1 : frame.pc + 1 + 2]
frame.pc += 2
var methodRefCpIndex uint16
err := binary.Read(bytes.NewBuffer(twoByteNum), binary.BigEndian, &methodRefCpIndex)
if nil != err {
return fmt.Errorf("failed to read method_ref_cp_index: %w", err)
}
// 取出引用的方法
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()
// 计算参数的个数
argCount := class.ParseArgCount(descriptor)
// 找到操作数栈中的引用, 此引用即为实际类型
// !!!如果有目标方法有参数, 则栈顶为参数而不是方法所在的实际对象,切记!!!
targetObjRef, _ := frame.opStack.GetObjectSkip(argCount)
targetDef := targetObjRef.Object.DefFile
// 调用
return i.ExecuteWithFrame(targetDef, methodName, descriptor, frame, true)
}
查找方法、创建栈帧、参数压栈等准备工作:
func (i *InterpretedExecutionEngine) ExecuteWithFrame(def *class.DefFile, methodName string, methodDescriptor string, lastFrame *MethodStackFrame, queryVTable bool) error {
// 查找方法
method, err := i.findMethod(def, methodName, methodDescriptor, queryVTable)
if nil != err {
return fmt.Errorf("failed to find method: %w", err)
}
// 因为method有可能是在父类中找到的,因此需要更新一下def到method对应的def
def = method.DefFile
// 解析访问标记
flagMap := accflag.ParseAccFlags(method.AccessFlags)
// ... 此处省略大量关于参数压栈、栈帧创建细节的代码 ...
// 执行字节码
return i.executeInFrame(def, codeAttr, frame, lastFrame, methodName, methodDescriptor)
}
从class中查找方法的完整实现:
// 查找方法定义;
// def: 当前class定义
// methodName: 目标方法简单名
// methodDescriptor: 目标方法描述符
// queryVTable: 是否只在虚方法表中查找
func (i *InterpretedExecutionEngine) findMethod(def *class.DefFile, methodName string, methodDescriptor string, queryVTable bool) (*class.MethodInfo, error) {
if queryVTable {
// 直接从虚方法表中查找
for _, item := range def.VTable {
if item.MethodName == methodName && item.MethodDescriptor == methodDescriptor {
return item.MethodInfo, nil
}
}
return nil, fmt.Errorf("method '%s' not found in VTable", methodName)
}
currentClassDef := def
for {
for _, method := range currentClassDef.Methods {
name := currentClassDef.ConstPool[method.NameIndex].(*class.Utf8InfoConst).String()
descriptor := currentClassDef.ConstPool[method.DescriptorIndex].(*class.Utf8InfoConst).String()
// 匹配简单名和描述符
if name == methodName && descriptor == methodDescriptor {
return method, nil
}
}
if 0 == def.SuperClass {
break
}
// 从父类中寻找
parentClassRef := currentClassDef.ConstPool[currentClassDef.SuperClass].(*class.ClassInfoConstInfo)
// 取出父类全名
targetClassFullName := currentClassDef.ConstPool[parentClassRef.FullClassNameIndex].(*class.Utf8InfoConst).String()
// 查找到Exception就止步, 目前还没有支持这个class的加载
if "java/lang/Exception" == targetClassFullName {
break
}
// 加载父类
parentDef, err := i.miniJvm.MethodArea.LoadClass(targetClassFullName)
if nil != err {
return nil, fmt.Errorf("failed to load superclass '%s': %w", targetClassFullName, err)
}
currentClassDef = parentDef
}
return nil, fmt.Errorf("method '%s' not found", methodName)
}
以下代码可以在 https://github.com/wanghongfei/mini-jvm/blob/master/vm/method_area.go 中找到。
虚方法表结构体的定义:
type VTableItem struct {
MethodName string
MethodDescriptor string
// 指向class元数据里的MethodInfo
MethodInfo *MethodInfo
}
虚方法表的构造:
// 为指定class初始化虚方法表;
// 此方法同时也会递归触发父类虚方法表的初始化工作, 但不会重复初始化
func (m *MethodArea) initVTable(def *class.DefFile) error {
def.VTable = make([]*class.VTableItem, 0, 5)
// 取出父类引用信息
superClassIndex := def.SuperClass
// 没有父类
if 0 == superClassIndex {
// 遍历方法元数据, 添加到虚方法表中
for _, methodInfo := range def.Methods {
// 取出方法访问标记
flagMap := accflag.ParseAccFlags(methodInfo.AccessFlags)
_, isPublic := flagMap[accflag.Public]
_, isProtected := flagMap[accflag.Protected]
_, isNative := flagMap[accflag.Native]
// 只添加public, protected, native方法
if !isPublic && !isProtected && !isNative {
// 跳过
continue
}
// 取出方法名和描述符
name := def.ConstPool[methodInfo.NameIndex].(*class.Utf8InfoConst).String()
descriptor := def.ConstPool[methodInfo.DescriptorIndex].(*class.Utf8InfoConst).String()
// 忽略构造方法
if name == "<init>" {
continue
}
newItem := &class.VTableItem{
MethodName: name,
MethodDescriptor: descriptor,
MethodInfo: methodInfo,
}
def.VTable = append(def.VTable, newItem)
}
return nil
}
superClassInfo := def.ConstPool[superClassIndex].(*class.ClassInfoConstInfo)
// 取出父类全名
superClassFullName := def.ConstPool[superClassInfo.FullClassNameIndex].(*class.Utf8InfoConst).String()
// 加载父类
superDef, err := m.LoadClass(superClassFullName)
if nil != err {
return fmt.Errorf("cannot load parent class '%s'", superClassFullName)
}
// 判断父类虚方法表是否已经初始化过了
if len(superDef.VTable) == 0 {
// 没有初始化过
// 初始化父类的虚方法表
err = m.initVTable(superDef)
if nil != err {
return fmt.Errorf("cannot init vtable for parent class '%s':%w", superClassFullName, err)
}
}
// 从父类虚方法表中继承元素
for _, superItem := range superDef.VTable {
subItem := &class.VTableItem{
MethodName: superItem.MethodName,
MethodDescriptor: superItem.MethodDescriptor,
MethodInfo: superItem.MethodInfo,
}
def.VTable = append(def.VTable, subItem)
}
// 遍历自己的方法元数据, 替换或者追加虚方法表
for _, methodInfo := range def.Methods {
// 取出方法名和描述符
name := def.ConstPool[methodInfo.NameIndex].(*class.Utf8InfoConst).String()
descriptor := def.ConstPool[methodInfo.DescriptorIndex].(*class.Utf8InfoConst).String()
// 忽略构造方法
if name == "<init>" {
continue
}
// 取出方法描述符
flagMap := accflag.ParseAccFlags(methodInfo.AccessFlags)
_, isPublic := flagMap[accflag.Public]
_, isProtected := flagMap[accflag.Protected]
_, isNative := flagMap[accflag.Native]
// 只添加public, protected, native方法
if !isPublic && !isProtected && !isNative {
// 跳过
continue
}
// 查找虚方法表中是否已经存在
found := false
for _, item := range def.VTable {
if item.MethodName == name && item.MethodDescriptor == descriptor {
// 说明def类重写了父类方法
// 替换虚方法表当前项
item.MethodInfo = methodInfo
found = true
break
}
}
if !found {
// 从父类继承的虚方法表中没找到此方法, 说明是子类的新方法, 追加
newItem := &class.VTableItem{
MethodName: name,
MethodDescriptor: descriptor,
MethodInfo: methodInfo,
}
def.VTable = append(def.VTable, newItem)
}
}
return nil
}
总结,invokevirtual
和虚方法表的实现是严重依赖于具体JVM实现所使用的数据结构的,很难单独摘出来看。但是我们可以根据文字描述的思路自己努力去实现,这也是Mini-JVM的初衷。