知識要點:
函數(shù)解析
靜態(tài)分派
動態(tài)分派
字節(jié)碼
函數(shù)解析
虛擬機方法字節(jié)碼
在Java語言中符合“編譯期可知,運行期不可變”。這個要求的方法,主要包括靜態(tài)方法和私有方法兩大類,前者與類型直接關(guān)聯(lián),后者在外部不可被訪問,這兩種方法各自的特點決定了它們都不可能通過繼承或別的方式重寫其他版本,因此它們都適合在類加載階段進行解析。
與之相對應(yīng)的是,在Java虛擬機里面提供了5條方法調(diào)用字節(jié)碼指令,分別如下:
- invokestatic 調(diào)用靜態(tài)方法。
- invokespecial 調(diào)用實例構(gòu)造器<init>方法、 私有方法和父類方法。
- invokevirtual 調(diào)用所有的虛方法。
- invokeinterface 調(diào)用接口方法,會在運行時再確定一個實現(xiàn)此接口的對象。
- invokedynamic 先在運行時動態(tài)解析出調(diào)用點限定符所引用的方法,然后再執(zhí)行該方法,在此之前的4條調(diào)用指令,分派邏輯是固化在Java虛擬機內(nèi)部的,而invokedynamic指令的分派邏輯是由用戶所設(shè)定的引導(dǎo)方法決定的。
靜態(tài)類型和實際類型
Human man=new Man();
上面代碼中的“Human”稱為變量的靜態(tài)類型(Static Type),或者叫做的外觀類型(Apparent Type),后面的“Man”則稱為變量的實際類型(Actual Type)
虛方法
- 非虛方法:只要能被invokestatic和invokespecial指令調(diào)用的方法,都可以在解析階段中確定唯一的調(diào)用版本,符合這個條件的有靜態(tài)方法、私有方法、實例構(gòu)造器、父類方法4類。
- 虛方法:除去final方法和非虛方法,其他方法稱為虛方法
虛方法表
虛方法表(Vritual Method Table,也稱為vtable,與此對應(yīng)的,在invokeinterface執(zhí)行時也會用到接口方法表itable)使用虛方法表索引來代替元數(shù)據(jù)查找以提高性能。虛方法表中存放著各個方法的實際入口地址。虛方法表一般是在類加載的階段進行初始化。

- 如果某個方法在子類中沒有被重寫,那子類的虛方法表里面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實現(xiàn)入口
- 如果子類中重寫了這個方法,子類方法表中的地址將會替換為指向子類實現(xiàn)版本的入口地址
靜態(tài)分派
所有依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動作稱為靜態(tài)分派。靜態(tài)分派的典型應(yīng)用是方法重載。靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分派的動作實際上不是由虛擬機來執(zhí)行的。
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
// overload(多分派)
public void sayHello(Human guy) {
System.out.println("hello,guy!");
}
public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
int i = 0;
int m = 1;
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch(); // new invokespecial
// 下面兩句函數(shù)調(diào)用是:運行時決定,還是編譯時決定?
// invokevirtual 靜態(tài)分派 編譯器決定
sr.sayHello(man); // invokevirtual
sr.sayHello(woman); // invokevirtual
}
}
編譯器在重載時是通過參數(shù)的靜態(tài)類型而不是實際類型作為判定依據(jù)的。并且靜態(tài)類型是編譯期可知的,因此,在編譯階段,Javac編譯器會根據(jù)參數(shù)的靜態(tài)類型決定使用哪個重載版本。
動態(tài)分派
不是根據(jù)靜態(tài)類型來確定,而是在運行時根據(jù)實際類型來決定函數(shù)的版本。
public class DynamicDispatch {
static class Human {
protected void sayHello(){
System.out.println("Human");
};
}
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) {
// 動態(tài)分派。不是根據(jù)靜態(tài)類型來確定,而是在運行時根據(jù)實際類型來決定函數(shù)的版本。
// 靜態(tài)類型 Human man
// 實際類型 new Man()
Human man = new Man(); // invokespecial
Human woman = new Woman(); // invokespecial
man.sayHello(); // invokevirtual
woman.sayHello(); // invokevirtual
man = new Woman(); // new Woman
man.sayHello(); // invokevirtual #n
}
}
Java語言是一門靜態(tài)多分派、 動態(tài)單分派的語言。
字節(jié)碼
Java虛擬機的字節(jié)碼指令由兩部分組成
- 操作碼,也即Opcode。由一個字節(jié)長度的、 代表著某種特定操作含義的數(shù)字。由于操作碼的長度只有一個字節(jié),所以操作碼總數(shù)不可能超過256條
- 操作數(shù),也即Operands。跟隨在操作碼之后的零至多個代表此操作所需參數(shù)。由于Java虛擬機采用面向操作數(shù)棧而不是寄存器架構(gòu),所以大多數(shù)的指令都不包含操作數(shù),只有一個操作碼。
字節(jié)碼指令分類
- 加法指令:iadd、 ladd、 fadd、 dadd
- 減法指令:isub、 lsub、 fsub、 dsub
- 乘法指令:imul、 lmul、 fmul、 dmul
- 除法指令:idiv、 ldiv、 fdiv、 ddiv
- 求余指令:irem、 lrem、 frem、 drem
- 取反指令:ineg、 lneg、 fneg、 dneg
- 位移指令:ishl、 ishr、 iushr、 lshl、 lshr、 lushr
- 按位或指令:ior、 lor
- 按位與指令:iand、 land
- 按位異或指令:ixor、 lxor
- 自增指令:iinc
- 比較指令:dcmpg、 dcmpl、 fcmpg、 fcmpl、 lcmp
字節(jié)碼指令參考網(wǎng)址:adress: