java多態的底層原理

虛擬機運行角度解釋多態實現原理 動態綁定、方法表

  1. 將一個方法調用同一個方法主體關聯起來被稱作綁定,JAVA中分爲前期綁定和後期綁定(動態綁定)
  2. 在程序執行之前進行綁定(由編譯器和連接程序實現)叫做前期綁定
    1. 因爲在編譯階段被調用方法的直接地址就已經存儲在方法所屬類的常量池中了,程序執行時直接調用 (invokestatic指令) ,如,final,static,private,構造方法,成員變量(包括靜態及非靜態)
  3. 後期綁定含義就是在程序運行時根據對象的類型信息進行綁定 實例調用 (invokevirtual) 
    1. 想實現後期綁定,需要在運行時能判斷對象的類型,從而找到對應的方法,即必須在對象中安置某種“類型信息”,JAVA中除了static方法、final方法(private方法屬於)之外,其他的方法都是後期綁定
    2. 後期綁定會涉及到JVM管理下【每個類裏都有】的一個重要的數據結構——方法表,方法表以數組的形式記錄當前類及其所有父類的可見方法字節碼在內存中的直接地址

 

方法區-虛擬機已加載的類信息

當程序運行需要某個類的定義時,載入子系統 (class loader subsystem) 裝入所需的 class 文件,並在內部建立該類的類型信息,這個類型信息就存貯在方法區

類型信息一般包括該類的方法代碼、類變量、成員變量的定義等等

 

在JVM執行Java字節碼時,“類型信息”被存放在方法區中,通常爲了優化對象調用方法的速度,方法區的類型信息中增加一個指針,指向方法表:

一張記錄該類的方法入口的表(稱爲方法表),表中的每一項都是指向相應方法的指針

 

方法表的構造如下:

  1. Java的單繼承機制,一個類只能繼承一個父類,而所有的類又都繼承自Object類。
  2. 方法表中最先存放的是Object類的方法,接下來是該類的父類的方法,最後是該類本身的方法。
  3. 這裏關鍵的地方在於,如果子類改寫了父類的方法,那麼子類和父類的那些同名方法共享一個方法表項,都被認作是父類的方法
  4. 由於以上方法的排列特性(Object——父類——子類),使得方法表的偏移量總是固定的

不包括靜態方法

注意這裏只有非私有的實例方法纔會出現,並且靜態方法也不會出現在這裏,原因很容易理解:靜態方法跟對象無關,可以將方法地址直接引用,而不像實例方法需要間接引用。

更深入地講,靜態方法是由虛擬機指令invoke static調用的,私有方法和構造函數則是由invoke special指令調用,只有被invoke virtual和invoke interface指令調用的方法纔會在方法表中出現

 

由於以上方法的排列特性(Object——父類——子類),使得方法表的偏移量總是固定的。

Person 或 Object 的任意一個方法,在它們(父類)的方法表和其子類 Girl 和 Boy 的方法表中的位置 (index) 是一樣的。

這樣 JVM 在調用實例方法其實只需要指定調用方法表中的第幾個方法即可

 

方法表中的表項,都是指向該類對應方法的指針,這裏就開始了多態的實現:

假設Class A是Class B的子類,並且A改寫了B的方法method()

那麼在B的方法表中,method方法的指針指向的就是B的method方法入口。

而對於A來說,它的方法表中的method方法則會指向其自身的method方法而非其父類的(這在類加載器載入該類時已經保證,同時JVM會保證總是能從對象引用指向正確的類型信息)

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

符號引用解析爲直接引用的過程

1.編譯類,在常量池方法索引信息表(CONSTANT_Methodref_info)中查找,生成方法調用的符號引用 12

當編譯 Party 類的時候,生成 girl.speak()的方法調用,尋找

Invokevirtual #12

設該調用代碼對應着 girl.speak(); #12 是 Party 類的常量池的符號引用,存儲在CONSTANT_Methodref_info 表。

2.進一步在各個表中查找,得出要調用的方法是 Person 的 speak 方法

JVM 首先查看 Party 的常量池索引爲 12 的條目( CONSTANT_Methodref_info 表中),進一步查看常量池(CONSTANT_Class_info,CONSTANT_NameAndType_info ,CONSTANT_Utf8_info)可得出方法的類型爲: Person 類型(注意引用 girl 是其基類 Person 類型)

3.獲取直接引用,在父類方法表中查找位置

查看 Person 的方法表,得出 speak 方法在該方法表中的偏移量 15(offset),這就是該方法調用的直接引用。

當解析出方法調用的直接引用後(方法表偏移量 15)

4.JVM 執行真正的方法調用,在子類方法表中調用

JVM 執行真正的方法調用:根據實例方法調用的參數 this 得到具體的對象(即 girl 所指向的位於堆中的對象),據此得到該對象對應的方法表 (Girl 的方法表 ),進而調用方法表中的某個偏移量所指向的方法(Girl 的 speak() 方法的實現)

 

結合方法指針偏移量是固定的以及指針總是指向實際類的方法域,我們不難發現多態的機制就在這裏:

方法調用過程:

  • 當某個方法被調用時,JVM 首先要查找相應的常量池,得到方法的符號引用,並查找調用類的方法表以確定該方法的直接引用,結果是該符號引用被解析爲直接引用即【方法表的偏移量】

 

根據類型信息的多態實現:

虛擬機通過對象引用得到方法區中類型信息的入口,查詢類的方法表,當將子類對象聲明爲父類類型時,形式上調用的是父類方法,此時虛擬機會從實際類的方法表中獲得該方法名對應的指針進而就能指向實際類的方法了

 

我們的故事還沒有結束,事實上上面的過程僅僅是利用繼承實現多態的內部機制,多態的另外一種實現方式:實現接口相比而言就更加複雜,原因在於,Java的單繼承保證了類的線性關係,而接口可以同時實現多個,這樣光憑偏移量就很難準確獲得方法的指針。所以在JVM中,多態的實例方法調用實際上有兩種指令:

 

接口調用

因爲 Java 類是可以同時實現多個接口的,而當用接口引用調用某個方法的時候,情況就有所不同了

Java 允許一個類實現多個接口,從某種意義上來說相當於多繼承,這樣同樣的方法在基類和派生類的方法表的位置就可能不一樣了

爲什麼區分指令

invokevirtual指令用於調用聲明爲類的方法;

invokeinterface指令用於調用聲明爲接口的方法;

可以看到,由於接口的介入,繼承自於接口 IDance 的方法 dance()在類 Dancer 和 Snake 的方法表中的位置已經不一樣了

顯然我們無法通過給出方法表的偏移量來正確調用 Dancer 和 Snake 的這個方法。這也是 Java 中調用接口方法有其專有的調用指令(invokeinterface)的原因

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

  1. invokeinterface #13
  2. JVM 首先查看常量池,獲取方法調用的符號引用(名稱、返回值等等),然後利用 this 指向的實例,得到該實例的方法表,進而搜索方法表來找到合適的方法地址。
  3. 因爲每次接口調用都要搜索方法表,所以從效率上來說,接口方法的調用總是慢於類方法的調用的

 

類加載

當程序運行需要某個類的定義時,載入子系統 (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 後兩種調用實現的考察。

常量池(constant pool)

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

 

 

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