本篇我們專注invokevirtual
這一條指令,先通過簡單粗暴的方式實(shí)現(xiàn)指令的功能,然后探究如何通過著名的虛方法表(Virtual Method Table)來進(jìn)行一些優(yōu)化。
指令含義
invokevirtual
用于調(diào)用除靜態(tài)方法、構(gòu)造方法、私有方法、接口方法外的所有方法。其指令的格式為:
invokevirtual = 182 (0xb6)
Format:
invokevirtual indexbyte1 indexbyte2
Operand Stack:
..., objectref, [arg1, [arg2 ...]] →
其中indexbyte1
和indexbyte2
為一個uint16類型的無符號整數(shù),表示常量池中方法引用常量的下標(biāo),這點(diǎn)跟invokestatic
是完全相同的。后面的"→"表示出棧順序,即執(zhí)行到invokevirtual
時操作數(shù)棧的狀態(tài)是方法參數(shù)在上,對象引用在下,其中方法參數(shù)是可選的,而且出棧時參數(shù)的順序會跟方法定義里的順序相反。
這里最重要的就是在objectref
中查找目標(biāo)方法的過程。我們先來一段簡單的代碼:
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對象的引用,然后調(diào)用被子類重寫了的say()
方法,這時候編譯出的字節(jié)碼如下:
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的字節(jié)碼用于創(chuàng)建Student對象并調(diào)用構(gòu)造方法,然后astore_1
則將剛創(chuàng)建的Student對象的引用保存到了本地變量表下標(biāo)為1的槽位中,緊接著aload_1
就將本地變量表里的Student引用壓入棧頂,這個就是前面JVM規(guī)范里提到的objectref
,同時也是say()
方法的真實(shí)接收者。這樣當(dāng)我們就可以從剛創(chuàng)建的Student對象中查找say()
方法了,而不是其父類Person。對于后面invokevirtual
跟著的兩個bytes所指向的常量池里的方法常量
// 方法引用常量
type MethodRefConstInfo struct {
Tag uint8
ClassIndex uint16
NameAndTypeIndex uint16
}
我們只關(guān)心里面的方法名和方法描述符,即NameAndTypeIndex
字段,忽略ClassIndex
,因?yàn)闂m斠呀?jīng)有方法真實(shí)接收者的引用了。
綜上,invokevirtual
指令查找方法的過程如下(省略訪問權(quán)限驗(yàn)證):
- 從操作數(shù)棧頂開始向下遍歷,找到
objectref
元素,取出(不彈出) - 取出
objectref
的class元數(shù)據(jù),遍歷方法組數(shù),查找方法名跟方法描述符都匹配的方法,如果找到了就直接返回,沒找到進(jìn)入下一步 - 判斷有沒有父類,如果有則在父類中執(zhí)行同樣的查找,如果都沒找到則拋出
NoSuchMethodException
異常
虛方法表
從上面的步驟可以看出,每次執(zhí)行invokevirtual
指令都要在class元數(shù)據(jù)中做大量的查找,并且由于MethodRefConstInfo
里并沒有直接保存方法名和描述符本身,而是保存了他們在常量池的索引,因此整個流程下來需要多次訪問常量池才能獲取到定位方法所需要的全部信息。對此我們可以使用虛方法表(Virtual Method Table)加以優(yōu)化。
VTable本質(zhì)上就是一個數(shù)組,數(shù)組的每個元素都保存了目標(biāo)方法的入口和一些其他方便JVM具體實(shí)現(xiàn)所使用的方法信息。對于Object類,他的虛方法表里就會只保存Object里的里的公開方法;對于子類來說,方法表里的數(shù)據(jù)項(xiàng)排序一定是父類方法在前,自己新定義的方法在后,如果自己重寫了父類的方法,那么只需要將前面的父類方法所在的數(shù)據(jù)項(xiàng)里的方法入口替換為子類自己的方法入口即可。
這里額外解釋一下到底什么是"方法入口"。方法入口的說法是一種宏觀且抽象的叫法,具體到代碼里以什么方式體現(xiàn)是因不同的JVM實(shí)現(xiàn)而不同的。例如,如果你用C實(shí)現(xiàn)JVM,那么方法入口可以是一個方法指針,不過在我們Go實(shí)現(xiàn)的Mini-JVM中,方法入口則是包含了字節(jié)碼的
MethodInfo
結(jié)構(gòu)體指針,這里面保存了字節(jié)碼數(shù)組因而可以直接遍歷解釋執(zhí)行。
那么虛方法表如何提高方法查找效率呢?具體有兩個層次的實(shí)現(xiàn),一是在查找方法時直接遍歷虛方法表而不是class元數(shù)據(jù),這樣就省去了多次訪問常量池的開銷,但是仍然需要一個個對比虛方法表里的數(shù)據(jù)項(xiàng)看看是不是要找的目標(biāo)方法;二是在方法第一次執(zhí)行時,我們可以將方法的符號引用直接替換為虛方法表數(shù)組的下標(biāo),這樣以后再調(diào)用就能一步到找到目標(biāo)方法,不需要任何遍歷操作了。
Mini-JVM暫未實(shí)現(xiàn)對虛方法表第二個層次的優(yōu)化,狗頭
代碼實(shí)現(xiàn)
以下片段摘自 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()
// 計(jì)算參數(shù)的個數(shù)
argCount := class.ParseArgCount(descriptor)
// 找到操作數(shù)棧中的引用, 此引用即為實(shí)際類型
// !!!如果有目標(biāo)方法有參數(shù), 則棧頂為參數(shù)而不是方法所在的實(shí)際對象,切記!!!
targetObjRef, _ := frame.opStack.GetObjectSkip(argCount)
targetDef := targetObjRef.Object.DefFile
// 調(diào)用
return i.ExecuteWithFrame(targetDef, methodName, descriptor, frame, true)
}
查找方法、創(chuàng)建棧幀、參數(shù)壓棧等準(zhǔn)備工作:
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)
}
// 因?yàn)閙ethod有可能是在父類中找到的,因此需要更新一下def到method對應(yīng)的def
def = method.DefFile
// 解析訪問標(biāo)記
flagMap := accflag.ParseAccFlags(method.AccessFlags)
// ... 此處省略大量關(guān)于參數(shù)壓棧、棧幀創(chuàng)建細(xì)節(jié)的代碼 ...
// 執(zhí)行字節(jié)碼
return i.executeInFrame(def, codeAttr, frame, lastFrame, methodName, methodDescriptor)
}
從class中查找方法的完整實(shí)現(xiàn):
// 查找方法定義;
// def: 當(dāng)前class定義
// methodName: 目標(biāo)方法簡單名
// methodDescriptor: 目標(biāo)方法描述符
// 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 中找到。
虛方法表結(jié)構(gòu)體的定義:
type VTableItem struct {
MethodName string
MethodDescriptor string
// 指向class元數(shù)據(jù)里的MethodInfo
MethodInfo *MethodInfo
}
虛方法表的構(gòu)造:
// 為指定class初始化虛方法表;
// 此方法同時也會遞歸觸發(fā)父類虛方法表的初始化工作, 但不會重復(fù)初始化
func (m *MethodArea) initVTable(def *class.DefFile) error {
def.VTable = make([]*class.VTableItem, 0, 5)
// 取出父類引用信息
superClassIndex := def.SuperClass
// 沒有父類
if 0 == superClassIndex {
// 遍歷方法元數(shù)據(jù), 添加到虛方法表中
for _, methodInfo := range def.Methods {
// 取出方法訪問標(biāo)記
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()
// 忽略構(gòu)造方法
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)
}
// 判斷父類虛方法表是否已經(jīng)初始化過了
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)
}
// 遍歷自己的方法元數(shù)據(jù), 替換或者追加虛方法表
for _, methodInfo := range def.Methods {
// 取出方法名和描述符
name := def.ConstPool[methodInfo.NameIndex].(*class.Utf8InfoConst).String()
descriptor := def.ConstPool[methodInfo.DescriptorIndex].(*class.Utf8InfoConst).String()
// 忽略構(gòu)造方法
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
}
// 查找虛方法表中是否已經(jīng)存在
found := false
for _, item := range def.VTable {
if item.MethodName == name && item.MethodDescriptor == descriptor {
// 說明def類重寫了父類方法
// 替換虛方法表當(dāng)前項(xiàng)
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
}
總結(jié),invokevirtual
和虛方法表的實(shí)現(xiàn)是嚴(yán)重依賴于具體JVM實(shí)現(xiàn)所使用的數(shù)據(jù)結(jié)構(gòu)的,很難單獨(dú)摘出來看。但是我們可以根據(jù)文字描述的思路自己努力去實(shí)現(xiàn),這也是Mini-JVM的初衷。