點贊關注,再也不迷路,你的支持對我意義重大!前端
🔥 Hi,我是醜醜。本文 GitHub · Android-NoteBook 已收錄,這裏有 Android 進階成長路線筆記 & 博客,歡迎跟着彭醜醜一塊兒成長。(聯繫方式在 GitHub)java
首先,嘗試寫出如下程序的輸出:git
public class Base {
public static void funcStatic(String str){
System.out.println("Base - funcStatic - String");
}
public static void funcStatic(Object obj){
System.out.println("Base - funcStatic - Object");
}
public void func(String str){
System.out.println("Base - func - String");
}
public void func(Object obj){
System.out.println("Base - func - Object");
}
}
public class Child extends Base {
public static void funcStatic(String str){
System.out.println("Child - funcStatic - String");
}
public static void funcStatic(Object obj){
System.out.println("Child - funcStatic - Object");
}
@Override
public void func(String str){
System.out.println("Child - func - String");
}
@Override
public void func(Object obj){
System.out.println("Child - func - Object");
}
}
複製代碼
public class Test{
public static void main(String[] args){
Object obj = new Object();
Object str = new String();
Base base = new Base();
Base child1 = new Child();
Child child2 = new Child();
base.funcStatic(obj); // 正常編程中不該該用實例去調用靜態方法
child1.funcStatic(obj);
child2.funcStatic(obj);
base.func(str);
child1.func(str);
child2.func(str);
}
}
複製代碼
程序輸出:github
Base - funcStatic - Object
Base - funcStatic - Object
Child - funcStatic - Object
Base - func - Object
Child - func - Object
Child - func - Object
複製代碼
程序輸出是否與你的預期一致呢?遇到困難了嗎,相信這篇文章必定能幫到你...面試
對於Java
編譯過程不瞭解,請閱讀:《Java | 聊一聊編譯過程(編譯前端 & 編譯後端)》編程
對於Class 文件 & 符號引用
不瞭解,請閱讀:《Java | 請概述一下 Class 文件的結構》後端
對於類加載
的流程不太瞭解,請閱讀:《Java | 談談你對類加載過程的理解》markdown
每個變量都有兩種類型:靜態類型(Static Type) & 實際類型(Actual Type)。例以下面代碼中,Base
爲變量base
的靜態類型,Child
爲實際類型:ide
Base base = new Child();
複製代碼
二者的具體區別以下:函數
這裏先談到這裏,後文會從字節碼的角度理解繼續討論兩個類型。
這一節,咱們來討論Java
中方法調用的本質。咱們知道,Java
前端編譯的產物是字節碼,與C/C++
不一樣,前端編譯過程當中並無連接步驟,字節碼中全部的方法調用都是使用符號引用。舉個例子:
- 源碼:
public class Child extends Base {
@Override
void func() {
}
void test1(){
func();
}
void test2(){
super.func();
}
}
- 字節碼(javap -c Child.class):
Compiled from "Child.java"
public class com.Child extends com.Base {
// 構造函數,默認調用父類構造函數
public com.Child();
Code:
0: aload_0
1: invokespecial #1 // Method com/Base."<init>":()V
4: return
void func();
Code:
0: return
void test1();
Code:
0: aload_0
// invokevirtual 調用實例方法
1: invokevirtual #2 // Method func:()V
4: return
void test2();
Code:
0: aload_0
// invokespecial 調用靜態方法
1: invokespecial #3 // Method com/Base.func:()V
4: return
}
複製代碼
上面的字節碼中,invokespecial
和invokevirtual
都是方法調用的字節碼指令,具體細節下文會詳細解釋。後面的#1 #2 #3
表示符號引用在常量池中的索引號,根據這個索引號檢索常量表,能夠查到最終表示的是一個字符串字面量,例如func:()V
,這個就是方法的符號引用。
爲了方便理解字節碼,
javap
反編譯的字節碼已經在註釋中提示了最終表示的值,例如Method func:()V
。
符號引用(Symbolic References)是一個用來無歧義地標識一個實體(例如方法/字段)的字符串,在運行期它會翻譯爲直接引用(Direct Reference)。對於方法來講,就是方法的入口地址。
下圖描述了方法符號引用的基本格式:
這個符號引用包含了變量的靜態類型(若是是變量的靜態類型與本類相同,不須要指明)、簡單方法名以及描述符(參數順序、參數類型和方法返回值)。經過這個符號引用,Java虛擬機就能夠翻譯出該方法的直接引用。可是,同一個符號引用,運行時翻譯出來的直接引用多是不一樣的,爲何會這樣呢?
1. 方法調用的本質是根據方法的符號引用肯定方法的直接引用(入口地址)
爲何同一個符號引用,運行時翻譯出來的直接引用多是不一樣的? 這與使用的方法調用指令的處理過程有關,Java
字節碼的方法調用指令一共有如下 5 種:
其中,根據調用方法的版本是否在編譯期能夠肯定,(注意:只是版本,而不是入口地址,入口地址只能在運行時肯定)能夠將方法調用劃分爲靜態解析 & 動態分派兩種。
# 誤區(重要)#
《深刻理解Java虛擬機》中將方法調用分爲解析、靜態分派、動態分派三種,又根據宗量的數量引入了靜態多分派,動態單分派的概念。這些概念事實上過於字典化,也很容易讓讀者誤認爲靜態分派與動態分派是非此即彼的互斥關係。事實上,一個方法能夠同時重寫與重載 ,重載 & 重寫是方法調用的兩個階段,而不是兩個種類。
下面,我將介紹Java
中方法選擇的三個步驟:
上一節咱們提到過方法符號引用的基本格式,分爲三個部分:
類的全限定名中將.
替換爲/
,例如java.lang.Object
對應java/lang/Object
方法的名稱,例如Object#toString()
的簡單名稱爲:toString
方法的參數列表和返回值,例如Object#toString()
的描述符爲()LJava/lang/String;
描述符的規則不是本文重點,這裏便再也不贅述了,若不瞭解可閱讀延伸文章。這裏咱們用兩段程序驗證上述規則,這兩段程序中咱們考慮了重載 & 重寫、靜態 & 實例兩個維度的因素:
程序一(重載 & 重寫)
public class Base {
public void func() {}
public void func(int i){}
}
public class Child extends Base {
@Override
public void func() {}
@Override
public void func(int i){}
}
public class Test{
public static void main(String[] args){
Base base1 = new Base();
Base child1 = new Child();
Child child2 = new Child();
base1.func(); // invokevirtual com.Base.func:():V
child1.func(); // invokevirtual com.Base.func:():V
child2.func(); // invokevirtual com.Child.func:():V
base1.func(1); // invokevirtual com.Base.func:(I):V
child1.func(1); // invokevirtual com.Base.func:(I):V
child2.func(1); // invokevirtual com.Child.func:(I):V
}
}
複製代碼
能夠看到,符號引用中的類名確實是變量的靜態類型,而不是變量的實際類型;方法名不用多說,方法描述符則選擇重載方法中最合適的一個方法。這個例程很容易判斷重載方法選擇結果,具體選擇規則其實更爲複雜。
程序二(靜態 & 實例)
public class Base {
public static void func() {}
public void func(int i){}
}
public class Child extends Base {
public static void func() {}
@Override
public void func(int i){}
}
public class Test{
public static void main(String[] args){
Base base1 = new Base();
Base child1 = new Child();
Child child2 = new Child();
符號引用與程序一相同,僅指令不一樣
base1.func(); // invokestatic com.Base.func:():V
child1.func(); // invokestatic com.Base.func:():V
child2.func(); // invokestatic com.Child.func:():V
base1.func(1); // invokevirtual com.Base.func:(I):V
child1.func(1); // invokevirtual com.Base.func:(I):V
child2.func(1); // invokevirtual com.Child.func:(I):V
}
}
複製代碼
能夠看到,static
對符號引用沒有影響,僅影響使用的指令(靜態方法調用使用invokestatic
)。而經過對象實例去調用靜態方法是javac
的語法糖,編譯時會轉換爲使用變量的靜態類型固化到符號引用中。
1. 方法的符號引用在編譯期肯定,並固化到字節碼中方法調用指令的參數中
2. 是否有static
修飾對符號引用沒有影響,僅影響使用的字節碼指令,對象實例去調用靜態方法是javac
的語法糖
爲何靜態方法、私有實例方法、實例構造器、父類方法以及final修飾這五種方法(對應的關鍵字: static、private、<init>、super、final
)能夠在編譯期肯定版本呢?由於不管運行時加載多少個類,這些方法都保證惟一的版本:
方法 | 緣由 |
---|---|
static |
相同簽名的子類方法會隱藏父類方法 |
private |
只在本類可見 |
<init> |
由編譯器生成,源碼沒法編寫 |
super |
Java 是單繼承,只有一個父類 |
final |
禁止被重寫 |
既然能夠肯定方法的版本,虛擬機在處理invokestatic
、invokespecial
、invokevirtual(final)
時,就能夠提早將符號引用轉換爲直接引用,沒必要延遲到方法調用時肯定,具體來講,是在類加載的解析階段完成轉換的。
1)類加載解析階段:根據符號引用中類名(以下例中java/lang/String
變量的靜態類型中),在對應的類中找到簡單名稱與描述符相符合的方法,若是找到則將符號引用轉換爲直接引用;不然,按照繼承關係從下往上依次在各個父類中搜索
2)調用階段:符號引用已經轉換爲直接引用;調用invokestatic
不須要將對象加載到操做數棧,只須要將所須要的參數入棧就能夠執行invokestatic
指令。例如:
源碼:
String str = String.valueOf("1")
字節碼:
0: iconst_1
1: invokestatic #2 // Method java/lang/String.valueOf:(I)Ljava/lang/String;
4: astore_1
複製代碼
1)類加載解析階段:同invokestatic
,也是從符號引用中的靜態類型開始查找
2)調用階段:同invokestatic
,符號引用已經轉換爲直接引用;、父類方法、私有實例方法這3種狀況都是屬於實例方法,因此調用invokespecial
指令須要將對象加載到操做數棧。例如:
一、源碼(實例構造器):
String str = new String();
字節碼:
0: new #2 // class java/lang/String
3: dup
4: invokespecial #3 // Method java/lang/String."<init>":()V
7: astore_1
--------------------------------------------------------------------
二、源碼(父類方法):
super.func();
字節碼:
0: aload_0
1: invokespecial #2 // Method com/Base.func:()V
--------------------------------------------------------------------
三、源碼(私有方法):
funcPrivate();
字節碼:
0: aload_0
1: invokespecial #2 // Method funPrivate:()V
複製代碼
動態分派分爲invokevitrual
、invokeinterface
與 invokedynamic
,其中動態調用invokedynamic
是 JDK 1.7 新增的指令,咱們單獨在另外一篇中解析。有些同窗可能會以爲方法不重寫不就只有一個版本了嗎?這個想法忽略了Java
動態連接的特性,Java
能夠從任何途徑加載一個class
,除非解析的 5 種的狀況外,沒法保證方法不被重寫。
虛擬機爲每一個類生成虛方法表vtable(virtual method table)
的結構,類中聲明的方法的入口地址會按固定順序存放在虛方法表中;虛方法表還會繼承父類的虛方法表,順序與父類保持一致,子類新增的方法按順序添加到虛方法末尾(這以Java
單繼承爲前提);若子類重寫父類方法,則重寫方法位置的入口地址修改成子類實現;
Class B
繼承與Class A
,並重寫了A
中的方法:Object
是全部類的父類,全部每一個類的虛方法表頭部都會包含Object
的虛方法表。另外,B
重寫了A#printMe()
,因此對應位置的入口地址方法被修改成B
重寫方法的入口地址。
須要注意的是,被final
、static
或private
修飾的方法不會出如今虛方法表中,由於這些方法沒法被繼承重寫。
接口方法的選擇行爲與類方法的選擇行爲略有區別,主要緣由是Java
接口是支持多繼承的,就沒辦法像虛方法表那樣直接繼承父類的虛方法表。虛擬機提供了itable(interface method table)
來支持多接口,itable
由偏移量表offset table
與方法表method table
兩部分組成。
當須要調用某個接口方法時,虛擬機會在offset table
查找對應的method table
,隨後在該method table
上查找方法。
invokestatic & invokespecial
能夠直接調用方法入口地址,最快invokevirtual
經過編號在vtable
中查找方法,次之invokeinterface
如今offset table
中查找method table
的偏移位置,隨後在method table
中查找接口方法的實現invokestatic & invokespecial
指令在(類加載時)解析時根據靜態類型完成轉換invokevirtual & invokeinterface
在(調用時)根據實際類型,查找vtable & itable
完成轉換vtable & itable
來支持虛方法的方法選擇。創做不易,你的「三連」是醜醜最大的動力,咱們下次見!