轉自:http://blog.csdn.net/fan2012huan/article/details/51007517
上兩篇篇博文討論了java的重載(overload)與重寫(override)、靜態分派與動態分派,這篇博文討論下動態分派的實現方法,即多態override的實現原理。
java方法調用之重載、重寫的調用原理(一)
java方法調用之單分派與多分派(二)
本文大部分內容來自於IBM的博文多態在 Java 和 C++ 編程語言中的實現比較 。這裏寫一遍主要是加深自己的理解,方便以後查看,加入了一些自己的見解及行文組織,不是出於商業目的,如若需要下線,請告知。
結論
基於基類的調用和基於接口的調用,從性能上來講,基於基類的調用性能更高 。因爲invokevirtual是基於偏移量的方式來查找方法的,而invokeinterface是基於搜索的。
概述
多態是面向對象程序設計的重要特性。多態允許基類的引用指向派生類的對象,而在具體訪問時實現方法的動態綁定。
java對方法動態綁定的實現方法主要基於方法表,但是這裏分兩種調用方式invokevirtual和invokeinterface,即類引用調用和接口引用調用。類引用調用只需要修改方法表的指針就可以實現動態綁定(具有相同簽名的方法,在父類、子類的方法表中具有相同的索引號),而接口引用調用需要掃描整個方法表才能實現動態綁定(因爲,一個類可以實現多個接口,另外一個類可能只實現一個接口,無法具有相同的索引號。這句如果沒有看懂,繼續往下看,會有例子。寫到這裏,感覺自己看書時,有的時候也會不理解,看不懂,思考一段時間,還是不明白,做個標記,繼續閱讀吧,然後回頭再看,可能就豁然開朗。)。
類引用調用的大致過程爲:java編譯器將java源代碼編譯成class文件,在編譯過程中,會根據靜態類型將調用的符號引用寫到class文件中。在執行時,JVM根據class文件找到調用方法的符號引用,然後在靜態類型的方法表中找到偏移量,然後根據this指針確定對象的實際類型,使用實際類型的方法表,偏移量跟靜態類型中方法表的偏移量一樣,如果在實際類型的方法表中找到該方法,則直接調用,否則,按照繼承關係從下往上搜索。
下面對上面的描述做具體的分析討論。
JVM的運行時結構
從上圖可以看出,當程序運行時,需要某個類時,類載入子系統會將相應的class文件載入到JVM中,並在內部建立該類的類型信息,這個類型信息其實就是class文件在JVM中存儲的一種數據結構,他包含着java類定義的所有信息,包括方法代碼,類變量、成員變量、以及本博文要重點討論的方法表。這個類型信息就存儲在方法區。
注意,這個方法區中的類型信息跟在堆中存放的class對象是不同的。在方法區中,這個class的類型信息只有唯一的實例(所以是各個線程共享的內存區域),而在堆中可以有多個該class對象。可以通過堆中的class對象訪問到方法區中類型信息。就像在java反射機制那樣,通過class對象可以訪問到該類的所有信息一樣。
方法表是實現動態調用的核心。方法表存放在方法區中的類型信息中。方法表中存放有該類定義的所有方法及指向方法代碼的指針。這些方法中包括從父類繼承的所有方法以及自身重寫(override)的方法。
類引用調用invokevirtual
代碼如下:
package org.fan.learn.methodTable;
/**
* Created by fan on 2016/3/30.
*/
public class ClassReference {
static class Person {
@Override
public String toString(){
return "I'm a person.";
}
public void eat(){
System.out.println("Person eat");
}
public void speak(){
System.out.println("Person speak");
}
}
static class Boy extends Person{
@Override
public String toString(){
return "I'm a boy";
}
@Override
public void speak(){
System.out.println("Boy speak");
}
public void fight(){
System.out.println("Boy fight");
}
}
static class Girl extends Person{
@Override
public String toString(){
return "I'm a girl";
}
@Override
public void speak(){
System.out.println("Girl speak");
}
public void sing(){
System.out.println("Girl sing");
}
}
public static void main(String[] args) {
Person boy = new Boy();
Person girl = new Girl();
System.out.println(boy);
boy.eat();
boy.speak();
//boy.fight();
System.out.println(girl);
girl.eat();
girl.speak();
//girl.sing();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
注意,boy.fight();
和 girl.sing();
這兩個是有問題的,在IDEA中會提示“Cannot resolve method ‘fight()’”。因爲,方法的調用是有靜態類型檢查的,而boy和girl的靜態類型都是Person類型的,在Person中沒有fight方法和sing方法。因此,會報錯。
執行結果如下:
從上圖可以看到,boy.eat()
和 girl.eat()
調用產生的輸出都是”Person eat”,因爲Boy和Girl中沒有override 父類的eat方法。
字節碼指令:
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=3, Args_size=1
0: new #2; //class ClassReference$Boy
3: dup
4: invokespecial #3; //Method ClassReference$Boy."<init>":()V
7: astore_1
8: new #4; //class ClassReference$Girl
11: dup
12: invokespecial #5; //Method ClassReference$Girl."<init>":()V
15: astore_2
16: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;
19: aload_1
20: invokevirtual #7; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
23: aload_1
24: invokevirtual #8; //Method ClassReference$Person.eat:()V
27: aload_1
28: invokevirtual #9; //Method ClassReference$Person.speak:()V
31: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;
34: aload_2
35: invokevirtual #7; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
38: aload_2
39: invokevirtual #8; //Method ClassReference$Person.eat:()V
42: aload_2
43: invokevirtual #9; //Method ClassReference$Person.speak:()V
46: return
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
其中所有的invokevirtual調用的都是Person類中的方法。
下面看看java對象的內存模型:
從上圖可以清楚地看到調用方法的指針指向。而且可以看出相同簽名的方法在方法表中的偏移量是一樣的。這個偏移量只是說Boy方法表中的繼承自Object類的方法、繼承自Person類的方法的偏移量與Person類中的相同方法的偏移量是一樣的,與Girl是沒有任何關係的。
下面再看看調用過程,以girl.speak()
方法的調用爲例。在我的字節碼中,這條指令對應43: invokevirtual #9; //Method ClassReference$Person.speak:()V
,爲了便於使用IBM的圖,這裏採用跟IBM一致的符號引用:invokevirtual #12;
。調用過程圖如下所示:
(1)在常量池中找到方法調用的符號引用
(2)查看Person的方法表,得到speak方法在該方法表的偏移量(假設爲15),這樣就得到該方法的直接引用。
(3)根據this指針確定方法接收者(girl)的實際類型
(4)根據對象的實際類型得到該實際類型對應的方法表,根據偏移量15查看有無重寫(override)該方法,如果重寫,則可以直接調用;如果沒有重寫,則需要拿到按照繼承關係從下往上的基類(這裏是Person類)的方法表,同樣按照這個偏移量15查看有無該方法。
接口引用調用invokeinterface
代碼如下:
package org.fan.learn.methodTable;
/**
* Created by fan on 2016/3/29.
*/
public class InterfaceReference {
interface IDance {
void dance();
}
static class Person {
@Override
public String toString() {
return "I'm a person";
}
public void speak() {
System.out.println("Person speak");
}
public void eat() {
System.out.println("Person eat");
}
}
static class Dancer extends Person implements IDance {
@Override
public String toString() {
return "I'm a Dancer";
}
@Override
public void speak() {
System.out.println("Dancer speak");
}
public void dance() {
System.out.println("Dancer dance");
}
}
static class Snake implements IDance {
@Override
public String toString() {
return "I'm a Snake";
}
public void dance() {
System.out.println("Snake dance");
}
}
public static void main(String[] args) {
IDance dancer = new Dancer();
System.out.println(dancer);
dancer.dance();
//dancer.speak();
//dancer.eat();
IDance snake = new Snake();
System.out.println(snake);
snake.dance();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
上面的代碼中dancer.speak(); dancer.eat();
這兩句同樣不能調用。
執行結果如下所示:
其字節碼指令如下所示:
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=3, Args_size=1
0: new #2; //class InterfaceReference$Dancer
3: dup
4: invokespecial #3; //Method InterfaceReference$Dancer."<init>":()V
7: astore_1
8: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
15: aload_1
16: invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V
21: new #7; //class InterfaceReference$Snake
24: dup
25: invokespecial #8; //Method InterfaceReference$Snake."<init>":()V
28: astore_2
29: getstatic #4; //Field java/lang/System.out:Ljava/io/PrintStream;
32: aload_2
33: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
36: aload_2
37: invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V
42: return
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
從上面的字節碼指令可以看到,dancer.dance();
和snake.dance();
的字節碼指令都是invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V
。
爲什麼invokeinterface指令會有兩個參數呢?
對象的內存模型如下所示:
從上圖可以看到IDance接口中的方法dance()在Dancer類的方法表中的偏移量跟在Snake類的方法表中的偏移量是不一樣的,因此無法僅根據偏移量來進行方法的調用。(這句話在理解時,要注意,只是爲了強調invokeinterface在查找方法時不再是基於偏移量來實現的,而是基於搜索的方式。)應該這麼說,dance方法在IDance方法表(如果有的話)中的偏移量與在Dancer方法表中的偏移量是不一樣的。
因此,要在Dancer的方法表中找到dance方法,必須搜索Dancer的整個方法表。
下面寫一個,如果Dancer中沒有重寫(override)toString方法,會發生什麼?
代碼如下:
package org.fan.learn.methodTable;
/**
* Created by fan on 2016/3/29.
*/
public class InterfaceReference {
interface IDance {
void dance();
}
static class Person {
@Override
public String toString() {
return "I'm a person";
}
public void speak() {
System.out.println("Person speak");
}
public void eat() {
System.out.println("Person eat");
}
}
static class Dancer extends Person implements IDance {
// @Override
// public String toString() {
// return "I'm a Dancer";
// }
@Override
public void speak() {
System.out.println("Dancer speak");
}
public void dance() {
System.out.println("Dancer dance");
}
}
static class Snake implements IDance {
@Override
public String toString() {
return "I'm a Snake";
}
public void dance() {
System.out.println("Snake dance");
}
}
public static void main(String[] args) {
IDance dancer = new Dancer();
System.out.println(dancer);
dancer.dance();
//dancer.speak();
//dancer.eat();
IDance snake = new Snake();
System.out.println(snake);
snake.dance();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
執行結果如下:
可以看到System.out.println(dancer);
調用的是Person的toString方法。
內存模型如下所示:
結束語
這篇博文討論了invokevirtual和invokeinterface的內部實現的區別,以及override的實現原理。下一步,打算討論下invokevirtual的具體實現細節,如:如何實現符號引用到直接引用的轉換的?可能會看下OpenJDK底層的C++實現。
參考資料
- 周志明 《深入理解JAVA虛擬機》
- IBM 多態在 Java 和 C++ 編程語言中的實現比較