JAVA虛擬機器動態連線及分派詳解
為什麼要將動態連線和分派放在一起講?
大家看完後面的內容這個問題迎刃而解了。
動態連線概括定義
每個棧幀都儲存了一個可以指向當前方法所在類的執行時常量池, 目的是當方法中需要呼叫其它方法的時候能夠從執行時常量池中找到對應的符號引用, 然後將符號引用轉換為直接引用然後就能直接呼叫對應的方法這就是動態連結,不是所有的方法呼叫都需要進行動態連結的 有一部分的符號引用會在類載入的解析階段將符號引用轉換為直接引用,這部分操作稱之為靜態解析。
靜態解析
類載入的解析階段會將部分的符號引用解析為直接引用,這部分的符號引用指的是編譯期間就能確定呼叫的版本,主要包括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: 呼叫 dispatch.sayHello(man); // Method test/jvm/DynamicDispatch$Human.sayHello:()V
20: 將Woman物件放入運算元棧頂
21: 呼叫虛方法 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也是一樣的