一直以來又長又臭的調(diào)用鏈簡直就是Java語言的標(biāo)志性特色,方法調(diào)用可謂是Java世界里表達(dá)一切邏輯的基石?,F(xiàn)在我們終于具備了實(shí)現(xiàn)它的基礎(chǔ)。
JVM中的5條方法調(diào)用指令
在JVM中觸發(fā)方法調(diào)用的指令有5條,分別是:
- invokestatic
調(diào)用靜態(tài)方法
- invokespecial
調(diào)用構(gòu)造方法
- invokeinterface
調(diào)用接口方法
- invokevirtual
調(diào)用對(duì)象方法
- invokedynamic
jdk1.7中引入,給動(dòng)態(tài)語言預(yù)留的調(diào)用指令。指令的第一個(gè)參數(shù)不再是代表方法符號(hào)引用的CONSTANT_Methodref_info常量,而是變?yōu)镴DK 1.7新加入的CONSTANT_InvokeDynamic_info常量
<br />
<br />
invokestatic指令的實(shí)現(xiàn)
這里面最簡單的就是invokestatic靜態(tài)方法調(diào)用指令了,因?yàn)殪o態(tài)方法不需要?jiǎng)?chuàng)建對(duì)象,屬于類。這條指令后面緊跟著兩個(gè)字節(jié),表示常量池中方法引用常量的下標(biāo):
invokestatic byte1 byte2
這樣的話我們就可以從常量池中找到CONSTANT_Methodref_info常量,結(jié)構(gòu)如下:
// 方法引用常量
type MethodRefConstInfo struct {
Tag uint8
ClassIndex uint16
NameAndTypeIndex uint16
}
可以看到,里面記錄了方法所在的類,和一個(gè)NameAndType常量的索引。其中NameAndType的結(jié)構(gòu)如下:
type NameAndTypeConst struct {
Tag uint8
NameIndex uint16
DescIndex uint16
}
其中NameIndex又是一個(gè)下標(biāo),指向的是常量池中的UTF8屬性,表示方法的簡單名,例如sayHello;
DescIndex也是一個(gè)下標(biāo),指向的是常量池中的UTF8屬性,表示方法描述符,例如(ILjava/lang/String;)Ljava/lang/String;。方法描述符描述了一個(gè)方法的參數(shù)類型、數(shù)量和返回類型,其中括號(hào)里的(ILjava/lang/String;)表示的是方法參數(shù),I表示基本類型int,Ljava/lang/String;表示對(duì)象類型。注意基本類型跟其他類型之間是沒有任何分隔符的,如果是對(duì)象類型,則以大寫字母L開頭,然后緊跟著類的全名,最后以;結(jié)束。括號(hào)外的Ljava/lang/String;表示此方法的返回類型也是String,如果沒有返回值則表示為V,沒有參數(shù)的話則表示為一對(duì)空括號(hào)()。綜上所述,描述符(ILjava/lang/String;)Ljava/lang/String;表示第一個(gè)參數(shù)為int類型,第二個(gè)參數(shù)為String類型,返回類型為String的方法:
String foo(int, String) {}
有了方法名、方法簽名、類名,我們就可以唯一確定一個(gè)方法了。在調(diào)用之前還要注意參數(shù)順序問題,調(diào)用前javac會(huì)生成一系列參數(shù)壓棧的指令,但是我們?cè)谌?shù)出棧的時(shí)候,由于棧先進(jìn)先出的性質(zhì),彈出參數(shù)的順序跟實(shí)際順序是相反的,這一點(diǎn)一定要小心。
最后我們?cè)賮砜匆幌鲁A砍刂械?code>UTF8數(shù)據(jù)項(xiàng)結(jié)構(gòu):
type Utf8InfoConst struct {
Tag uint8
Length uint16
Bytes []byte
}
里面的Bytes就是UTF8字節(jié)流了,可以直接轉(zhuǎn)換成string。class里所有對(duì)字符串的記錄都是通過UTF8屬性保存的,想引用這個(gè)字符串的話就用UTF8屬性在常量池的下標(biāo)來引用。
<br />
<br />
有了上面的分析,我們就可以按圖索驥的去實(shí)現(xiàn)了。首先,我們要把指令后面的byte1 byte2組裝回整數(shù):
methodRefCpIndex := (indexbyte1 << 8) | indexbyte2
然后取出方法引用常量、取出方法名、方法描述符、方法所在的class全名:
/ 取出引用的方法
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()
// 取出方法所在的class
classRef := def.ConstPool[methodRef.ClassIndex].(*class.ClassInfoConstInfo)
// 取出目標(biāo)class全名
targetClassFullName := def.ConstPool[classRef.FullClassNameIndex].(*class.Utf8InfoConst).String()
加載class:
// 加載
targetDef, err := i.miniJvm.findDefClass(targetClassFullName)
if nil != err {
return fmt.Errorf("failed to load class for '%s': %w", targetClassFullName, err)
}
這里的findDefClass()方法會(huì)首先從已經(jīng)加載的類中查找,如果沒有,就會(huì)遍歷classpath下的所有.class文件(包括jar包中的文件),找到全限定性名相符的class文件,執(zhí)行class解析邏輯(第二篇中有講),然后返回一個(gè)DefClass結(jié)構(gòu)的指針。
然后就可以直接調(diào)用方法了:
// 調(diào)用
return i.ExecuteWithFrame(targetDef, methodName, descriptor, frame)
在ExecuteWithFrame方法中實(shí)現(xiàn)了方法描述符的解析、方法棧幀的創(chuàng)建和參數(shù)的傳遞,代碼比較復(fù)雜不再羅列了,可以在這里找到完整代碼:https://github.com/wanghongfei/mini-jvm/blob/master/vm/interpreted_execution_engine.go
<br />
<br />
至此我們就完成了JVM中最簡單的方法調(diào)用指令的實(shí)現(xiàn)。為什么說最簡單呢,因?yàn)?code>invokestatic不需要?jiǎng)?chuàng)建對(duì)象,也不涉及復(fù)雜的方法查找邏輯,只需要匹配類名、方法名、方法簽名就可以了。靜態(tài)方法調(diào)用的實(shí)現(xiàn)是以后實(shí)現(xiàn)native方法的基石,在下一篇中就會(huì)介紹。