動态連接配接及分派深入詳解
- 為什麼要将動态連接配接和分派放在一起講?
- 動态連接配接概括定義
- 靜态解析
- 分派
-
- 靜态分派
- 動态分派
為什麼要将動态連接配接和分派放在一起講?
大家看完後面的内容這個問題迎刃而解了。
動态連接配接概括定義
每個棧幀都儲存了一個可以指向目前方法所在類的運作時常量池, 目的是當方法中需要調用其它方法的時候能夠從運作時常量池中找到對應的符号引用, 然後将符号引用轉換為直接引用然後就能直接調用對應的方法這就是動态連結,不是所有的方法調用都需要進行動态連結的
有一部分的符号引用會在類加載的解析階段将符号引用轉換為直接引用,這部分操作稱之為靜态解析。
靜态解析
類加載的解析階段會将部分的符号引用解析為直接引用,這部分的符号引用指的是編譯期間就能确定調用的版本,主要包括2大類
- invokestatic: 調用靜态方法
- invokespecial: 調用實列構造器
私有方法,私有方法,父類方法<init>
因為這2類不允許被重寫修改, 符合"編譯器可知,運作期不可變"的準則,把這類方法稱為非虛方法
除去靜态解析能在類加載的解析階段将符号引用解析為直接引用,剩下的符号引用就要在運作期間進行解析。
分派
在運作期間,或者靜态解析的時候,确定調用方法的時候方法就可能存在重載,重寫等情況這裡的分派将揭開"重載"和"重寫"的實作原理以及他們的選用規則
靜态分派
這裡主要揭開了重載的實作規則
public class StaticDispatch {
public void sayHello(Human human) {
System.out.println("human hello world!");
}
public void sayHello(Man human) {
System.out.println("Man hello world!");
}
public void sayHello(Woman human) {
System.out.println("Woman hello world!");
}
public static void main(String[] args) {
StaticDispatch dispatch = new StaticDispatch();
Human man = new Man();
Human woman = new Woman();
dispatch.sayHello(man);
dispatch.sayHello(woman);
}
}
abstract class Human {
}
class Man extends Human {
}
class Woman extends Human {
}
輸出:
human hello world!
human hello world!
為什麼會輸出這2個結果呢?主要是因為在編譯期間Human的類型是确定的我們稱之為靜态類型,Man是隻有在運作期間new Man()這個動作發生了後才知道它的具體類型我們稱為實際類型,而重載是根據靜态類型确定調用過程的是以都會去調用sayHello(Human human)。
這裡要強調一點的是分派是确定調用方法的過程,在類的解析階段,靜态方法也存在重載也可以使用靜态分派進行确定,在動态連接配接的确定方法調用版本的時候,也存在用分派确定調用,是以解析和分派不是分開進行的而是互相協作的。
動态分派
這裡主要揭開了重寫的實作規則
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("Man say hello !");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("Woman say hello !");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
}
}
輸出:
Man say hello !
Woman say hello !
相信大家一眼都能看出正确的結果,那麼虛拟機是如何知道調用的哪個方法呢?下面來看看javap輸出的位元組碼
public class test.jvm.DynamicDispatch
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#30 // java/lang/Object."<init>":()V
#2 = Class #31 // test/jvm/DynamicDispatch$Man
#3 = Methodref #2.#30 // test/jvm/DynamicDispatch$Man."<init>":()V
#4 = Class #32 // test/jvm/DynamicDispatch$Woman
#5 = Methodref #4.#30 // test/jvm/DynamicDispatch$Woman."<init>":()V
#6 = Methodref #12.#33 // test/jvm/DynamicDispatch$Human.sayHello:()V
#7 = Class #34 // test/jvm/DynamicDispatch
#8 = Class #35 // java/lang/Object
#9 = Utf8 Woman
...剩下的常量池的内容省略了
{
public test.jvm.DynamicDispatch();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 21: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ltest/jvm/DynamicDispatch;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class test/jvm/DynamicDispatch$Man
3: dup
4: invokespecial #3 // Method test/jvm/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4 // class test/jvm/DynamicDispatch$Woman
11: dup
12: invokespecial #5 // Method test/jvm/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method test/jvm/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method test/jvm/DynamicDispatch$Human.sayHello:()V
24: return
LineNumberTable:
line 44: 0
line 45: 8
line 46: 16
line 47: 20
line 48: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
8 17 1 man Ltest/jvm/DynamicDispatch$Human;
16 9 2 woman Ltest/jvm/DynamicDispatch$Human;
}
0-7: 建立Man對象并将其存入局部變量表
8-15: 建立Woman對象并将其存入局部變量表
16: 将Man對象放入操作數棧頂
17: 取出棧頂Man對象調用虛方法, dispatch.sayHello(man); // Method test/jvm/DynamicDispatch$Human.sayHello:()V
20: 将Woman對象放入操作數棧頂
21: 取出棧頂Woman對象調用虛方法 dispatch.sayHello(woman); // Method test/jvm/DynamicDispatch$Human.sayHello:()V
這裡的行号其實就是程式計數器執行完了一個指令後程式計數器下移繼續執行下一行指令
我們可以看到位元組碼行号為17, 20的時候都是調用的Human.sayHello那麼他是如何標明執行man還是woman的呢,這就要根據虛方法的通路規則來看了!具體如下
- 找到操作數棧頂第一個元素指向的對象标記為C
- 在C中尋找與之比對的方法,如果找到了就傳回其直接引用,但是因為通路權限這個方法不能通路會抛出IllegalAccessError異常。
- 如果沒有找到就會對其父類進行第2步的查找
- 如果最終都沒有找到合适的方法就會抛出java.lang.AbstractMethodError
看到這裡相信大家都能明白了
當執行17行号的代碼的時候此時操作數棧頂是Man對象,那麼就會執行一遍以上的搜尋成功調用Man的sayHello
同理調用Woman也是一樣的