##前言
多態是Java語言重要的特性之一,它允許基類的指針或引用指向派生類的對象,而在具體訪問時實現方法的動態綁定。Java對于方法調用動態綁定的實現主要依賴于方法表,但通過引用調用(invokevitual)和接口引用調用(invokeinterface)的實現則有所不同。
Java多態實現原理的大致過程:首先是Java編譯器將Java源代碼編譯成class文件。在編譯過程中,會根據靜態類型將調用的符號引用寫到class文件中。在執行時,JVM根據class文件找到調用方法的符號引用,然后在靜態類型的方法表中找到偏移量,然后再根據this指針確定對象的實際類型,使用實際類型的方法表(偏移量跟靜態類型中的偏移量一樣是指?就是用的靜態類型中的偏移量,因為符號引用在靜態類型的方法表中找到的偏移量是同一個),如果在實際的方法中找到該方法(說明參數值對上了)則直接調用,否則認為沒有重寫父類的方法則按照繼承關系從下往上搜索來調用方法。
程序運行時,需要某個類是,類載入系統會將相應的class文件載入到JVM中,并在內部建立該類的?類型信息 (這個類型信息其實就是class文件在JVM中存儲的一種數據結構),包含java類定義的所有信息(方法代碼、類和成員變量、以及實現動態調用的核心 -?方法表 )。這個類型信息存儲在方法區。
注意:這個方法去中的類型信息跟在堆中存放的class對象是不同的。在方法區中,這個class的類型信息只有唯一的實例(所以是各個線程共享的內存區域),而在堆中可以有多個該class對象。可以通過堆中的class對象訪問到方法去中的類型信息(像Java的反射機制,通過class對象可以訪問到該類的所有信息)。
【重點】
方法表是實現動態調用的核心。上面講過方法表存放在方法區中的類型信息中。為了優化對象調用方法的速度,方法區的類型信息會增加一個指針,該指針指向一個記錄該類方法的方法表,方法表中的每一個項都是對應方法的指針。
這些方法中包括從父類繼承的所有方法以及自身重寫(override)的方法。
【拓展】
方法區:方法區和JAVA堆一樣,是各個線程共享的內存區域,用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。?
運行時常量池:它是方法區的一部分,Class文件中除了有類的版本、方法、字段等描述信息外,還有一項信息是常量池,用于存放編譯器生成的各種符號引用,這部分信息在類加載時進入方法區的運行時常量池中。?
方法區的內存回收目標是針對常量池的回收及對類型的卸載。
#####Java?的方法調用方式
Java?的方法調用有兩類,動態方法調用與靜態方法調用。
- 靜態方法調用是指對于類的靜態方法的調用方式,是靜態綁定的
- 動態方法調用需要有方法調用所作用的對象,是動態綁定的。
類調用?(invokestatic)?是在編譯時就已經確定好具體調用方法的情況。
實例調用?(invokevirtual)則是在調用的時候才確定具體的調用方法,這就是動態綁定,也是多態要解決的核心問題。
JVM?的方法調用指令有四個,分別是?invokestatic,invokespecial,invokesvirtual?和?invokeinterface。前兩個是靜態綁定,后兩個是動態綁定的。本文也可以說是對于JVM后兩種調用實現的考察。
方法表與方法調用
如有類定義?Person, Girl, Boy
class Person {public String toString() {return "I'm a person.";}public void eat() {}public void speak() {}
}class Boy extends Person {public String toString() {return "I'm a boy";}public void speak() {}public void fight() {}
}class Girl extends Person {public String toString() {return "I'm a girl";}public void speak() {}public void sing() {}
}
當這三個類被載入到?Java?虛擬機之后,方法區中就包含了各自的類的信息。Girl?和?Boy?在方法區中的方法表可表示如下:
可以看到,Girl?和?Boy?的方法表包含繼承自 Object 的方法,繼承自直接父類 Person 的方法及各自新定義的方法。注意方法表條目指向的具體的方法地址,如?Girl?繼承自?Object?的方法中,只有?toString()?指向自己的實現(Girl?的方法代碼),其余皆指向?Object?的方法代碼;其繼承自于?Person?的方法?eat()?和?speak()?分別指向?Person?的方法實現和本身的實現。
如果子類改寫了父類的方法,那么子類和父類的那些同名的方法共享一個方法表項。
因此,方法表的偏移量總是固定的。所有繼承父類的子類的方法表中,其父類所定義的方法的偏移量也總是一個定值。
Person?或?Object中的任意一個方法,在它們的方法表和其子類?Girl?和?Boy?的方法表中的位置 (index) 是一樣的。這樣?JVM?在調用實例方法其實只需要指定調用方法表中的第幾個方法即可。
如調用如下:
class Party {void happyHour() {Person girl = new Girl();girl.speak();}
}
當編譯?Party?類的時候,生成?girl.speak()的方法調用假設為:????Invokevirtual #12
設該調用代碼對應著?girl.speak(); #12?是?Party?類的常量池的索引。JVM?執行該調用指令的過程如下所示:
(這里有個錯誤,上圖為ClassReference常量池而非Party的常量池)
【再次拓展】
常量池在邏輯上可以分成多個表,每個表包含一類的常量信息,本文只探討對于 Java 調用相關的常量池表。
CONSTATNT_Method_info**:**類方法引用表;包含引用的任何類型方法的描述信息,主要包括類信息索引和名字類型索引。
CONSTATNT_Class_info**:**類信息表;包含任何被引用的類或接口的 ‘符號引用’ ,每一個條目主要包含一個索引,指向CONSTA_Utf8_info表,表示該類或接口的全限定名。
CONSTATNT_NameAndType_info:名字類型表;包含引用的任意方法或字段的名稱和描述符信息在字符串常量中的索引。
CONSTATNT_Utf8_info:字符串常量表; 該表包含該類所使用的所有字符串常量,比如代碼中的字符串引用、引用的類名、方法的名字、其他引用的類與方法的字符串描述等等。其余常量池表中所涉及到的任何常量字符串都被索引至該表。
可以看到,給定任意一個方法的索引,在常量池中找到對應的條目后,可以得到該方法的類索引(classindex)和名字類型索引 (nameandtypeindex), 進而得到該方法所屬的類型信息和名稱及描述符信息(參數,返回值等)——從而通過對方法的類型信息和名稱及描述符信息(參數,返回值等)來確定具體是調用哪一個方法。
JVM執行??Invokevirtual #12?指令的過程:
(1)在常量池中找到方法調用的符號引用。?JVM 首先查看 Party(應為ClassReference常量池) 的常量池索引為 12 的條目 (此條目即指 -?查看常量池中的CONSTATNT_Method_info表,即類方法引用表),再 進一步查看常量池中的(CONSTANTClassinfo,CONSTANTNameAndTypeinfo ,CONSTANTUtf8info)?三個表。
(2) 可得出要調用的方法是 Person 的 speak 方法, 查看 Person 的方法表,得出 speak 方法在該方法表中的偏移量 15,這就是該方法調用的直接引用。
(3)?根據this指針得到具體的對象(即girl所指向位與堆中的對象)
(4)根據對象得到該對象對應的方法表,根據偏移量15查看有無重寫(override)該方法,如果重寫,則可以直接調用(Girl的方法表的speak項指向自身的方法而非父類);如果沒有重寫,則需要拿到按照繼承關系從下往上的基類(這里是Person類)的方法表,同樣按照這個偏移量15查看有無該方法。
##最后
以上,是對Java多態實現原理翻閱兩篇博文后為便于理解而整理而出。
參考博文:
https://www.cnblogs.com/kaleidoscope/p/9790766.html
https://zhuanlan.zhihu.com/p/94086109
大家看完有什么不懂的可以在下方留言討論.
謝謝你的觀看。
讀者福利
讀到這的朋友還可以免費領取一份收集的Java進階知識筆記和視頻資料。
資料免費領取方式:關注后,點擊這里即可免費領取
更多筆記分享
[外鏈圖片轉存中…(img-RDB9BwcB-1623502351596)]
更多筆記分享
[外鏈圖片轉存中…(img-dSYh4L64-1623502351597)]
[外鏈圖片轉存中…(img-pDxH0Vjj-1623502351598)]