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