從JVM的角度來看Java多態的底層原理

前言

  繼承和實現是我們平時使用最多的基礎內容之二吧,那麼這兩者的底層實現原理到底是什麼呢?從JVM的角度如何看繼承和實現呢?接下來我們一起來學習一下。


敘述

從JVM結構開始談多態

  Java對於方法調用動態綁定的實現主要依賴於方法表,但通過類引用調用和接口引用調用的實現則有所不同。總體而言,當某個方法被調用時,JVM 首先要查找相應的常量池,得到方法的符號引用,並查找調用類的方法表以確定該方法的直接引用,最後才真正調用該方法.

JVM 的結構

在這裏插入圖片描述
  此結構中,我們只探討和本文密切相關的方法區 (method area)。當程序運行需要某個類的定義時,載入子系統 (class loader subsystem) 裝入所需的 class 文件,並在內部建立該類的類型信息,這個類型信息就存貯在方法區。類型信息一般包括該類的方法代碼、類變量、成員變量的定義等等。可以說,類型信息就是類的 Java 文件在運行時的內部結構,包含了改類的所有在 Java 文件中定義的信息。

  注意到,該類型信息和 class 對象是不同的。class 對象是 JVM 在載入某個類後於堆 (heap) 中創建的代表該類的對象,可以通過該 class 對象訪問到該類型信息。比如最典型的應用,在 Java 反射中應用 class 對象訪問到該類支持的所有方法,定義的成員變量等等。可以想象,JVM 在類型信息和 class 對象中維護着它們彼此的引用以便互相訪問。兩者的關係可以類比於進程對象與真正的進程之間的關係。

Java 的方法調用方式

  Java 的方法調用有兩類,動態方法調用與靜態方法調用。靜態方法調用是指對於類的靜態方法的調用方式,是靜態綁定的;而動態方法調用需要有方法調用所作用的對象,是動態綁定的。類調用 (invokestatic) 是在編譯時刻就已經確定好具體調用方法的情況,而實例調用 (invokevirtual) 則是在調用的時候才確定具體的調用方法,這就是動態綁定,也是多態要解決的核心問題。

  JVM 的方法調用指令有四個,分別是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前兩個是靜態綁定,後兩個是動態綁定的。通過本文我們來看一下JVM中動態綁定的實現。

常量池

常量池中保存的是一個 Java 類引用的一些常量信息,包含一些字符串常量及對於類的符號引用信息等。Java 代碼編譯生成的類文件中的常量池是靜態常量池,當類被載入到虛擬機內部的時候,在內存中產生類的常量池叫運行時常量池。

常量池在邏輯上可以分成多個表,每個表包含一類的常量信息,本文只探討對於 Java 調用相關的常量池表。

CONSTANT_Utf8_info

字符串常量表,該表包含該類所使用的所有字符串常量,比如代碼中的字符串引用、引用的類名、方法的名字、其他引用的類與方法的字符串描述等等。其餘常量池表中所涉及到的任何常量字符串都被索引至該表。

CONSTANT_Class_info

類信息表,包含任何被引用的類或接口的符號引用,每一個條目主要包含一個索引,指向 CONSTANT_Utf8_info 表,表示該類或接口的全限定名。

CONSTANT_NameAndType_info

名字類型表,包含引用的任意方法或字段的名稱和描述符信息在字符串常量表中的索引。

CONSTANT_InterfaceMethodref_info

接口方法引用表,包含引用的任何接口方法的描述信息,主要包括類信息索引和名字類型索引。

CONSTANT_Methodref_info

類方法引用表,包含引用的任何類型方法的描述信息,主要包括類信息索引和名字類型索引。

常量池各表的關係:
在這裏插入圖片描述

方法表與方法調用

  多態的底層實現是動態綁定,即在運行時才把方法調用與方法實現關聯起來。

  方法表是動態調用(綁定)的核心,也是 Java 實現動態調用的主要方式。它被存儲於方法區中的類型信息,包含有該類型所定義的所有方法及指向這些方法代碼的指針,注意這些具體的方法代碼可能是被覆寫的方法,也可能是繼承自基類的方法。

如有類定義 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 在調用實例方法其實只需要指定調用方法表中的第幾個方法即可。
如調用如下:

清單 2

 class Party{void happyHour(){ 
 Person girl = new Girl(); 
 girl.speak();} 
 }

當編譯 Party 類的時候,生成 girl.speak()的方法調用假設爲:

Invokevirtual #12

設該調用代碼對應着 girl.speak(); #12 是 Party 類的常量池的索引。JVM 執行該調用指令的過程如下所示:
在這裏插入圖片描述
JVM 首先查看 Party 的常量池索引爲 12 的條目(應爲 CONSTANT_Methodref_info 類型,可視爲方法調用的符號引用),進一步查看常量池(CONSTANT_Class_info,CONSTANT_NameAndType_info ,CONSTANT_Utf8_info)可得出要調用的方法是 Person 的 speak 方法(注意引用 girl 是其基類 Person 類型),查看 Person 的方法表,得出 speak 方法在該方法表中的偏移量 15(offset),這就是該方法調用的直接引用。

當解析出方法調用的直接引用後(方法表偏移量 15),JVM 執行真正的方法調用:根據實例方法調用的參數 this 得到具體的對象(即 girl 所指向的位於堆中的對象),據此得到該對象對應的方法表 (Girl 的方法表 ),進而調用方法表中的某個偏移量所指向的方法(Girl 的 speak() 方法的實現)。
接口調用
因爲 Java 類是可以同時實現多個接口的,而當用接口引用調用某個方法的時候,情況就有所不同了。Java 允許一個類實現多個接口,從某種意義上來說相當於多繼承,這樣同樣的方法在基類和派生類的方法表的位置就可能不一樣了。

接口調用

因爲 Java 類是可以同時實現多個接口的,而當用接口引用調用某個方法的時候,情況就有所不同了。Java 允許一個類實現多個接口,從某種意義上來說相當於多繼承,這樣同樣的方法在基類和派生類的方法表的位置就可能不一樣了。

interface IDance{ 
   void dance(); 
 } 

 class Person { 
 public String toString(){ 
   return "I'm a person."; 
     } 
 public void eat(){} 
 public void speak(){} 

 } 

 class Dancer extends Person 
 implements IDance { 
 public String toString(){ 
   return "I'm a dancer."; 
     } 
 public void dance(){} 
 } 

 class Snake implements IDance{ 
 public String toString(){ 
   return "A snake."; 
     } 
 public void dance(){ 
 //snake dance 
     } 
 }

在這裏插入圖片描述
可以看到,由於接口的介入,繼承自於接口 IDance 的方法 dance()在類 Dancer 和 Snake 的方法表中的位置已經不一樣了,顯然我們無法通過給出方法表的偏移量來正確調用 Dancer 和 Snake 的這個方法。這也是 Java 中調用接口方法有其專有的調用指令(invokeinterface)的原因。

Java 對於接口方法的調用是採用搜索方法表的方式,對如下的方法調用

invokeinterface #13

JVM 首先查看常量池,確定方法調用的符號引用(名稱、返回值等等),然後利用 this 指向的實例得到該實例的方法表,進而搜索方法表來找到合適的方法地址。

因爲每次接口調用都要搜索方法表,所以從效率上來說,接口方法的調用總是慢於類方法的調用的。
執行結果如下:
在這裏插入圖片描述
可以看到System.out.println(dancer); 調用的是Person的toString方法。

小結

本篇博客的內容來自於繼承、封裝、多態的實現原理.md
感謝您的閱讀~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章