JVM方法调用说明
原文鏈接:http://www.jianshu.com/p/56a7c4b26b14
前言
Java具備三種特性:封裝、繼承、多態(tài)。
Java文件在編譯過程中不會進(jìn)行傳統(tǒng)編譯的連接步驟,方法調(diào)用的目標(biāo)方法以符號引用的方式存儲在Class文件中,這種多態(tài)特性給Java帶來了更靈活的擴(kuò)展能力,但也使得方法調(diào)用變得相對復(fù)雜,需要在類加載期間,甚至到運(yùn)行期間才能確定目標(biāo)方法的直接引用。
方法調(diào)用
所有方法調(diào)用的目標(biāo)方法在Class文件里面都是常量池中的符號引用。在類加載的解析階段,如果一個方法在運(yùn)行之前有確定的調(diào)用版本,且在運(yùn)行期間不變,虛擬機(jī)會將其符號引用解析為直接調(diào)用。
這種 編譯期可知,運(yùn)行期不可變 的方法,主要包括靜態(tài)方法和私有方法兩大類,前者與具體類直接關(guān)聯(lián),后者在外部不可訪問,兩者都不能通過繼承或別的方式進(jìn)行重寫。
JVM提供了如下方法調(diào)用字節(jié)碼指令:
通過invokestatic和invokespecial指令調(diào)用的方法,可以在解析階段確定唯一的調(diào)用版本,符合這種條件的有靜態(tài)方法、私有方法、實例構(gòu)造器和父類方法4種,它們在類加載時會把符號引用解析為該方法的直接引用。
invokestatic
class StaticTest {public static void hello() {System.out.println("hello");}public static void main(String args[]) {hello();} }通過javap命令查看main方法字節(jié)碼
可以發(fā)現(xiàn)hello方法是通過invokestatic指令調(diào)用的。
invokespecial
class VirtualTest {private int id;public static void main(String args[]) {new VirtualTest();} }通過javap命令查看main方法字節(jié)碼
可以發(fā)現(xiàn)實例構(gòu)造器是通過invokespecial指令調(diào)用的。
通過invokestatic和invokespecial指令調(diào)用的方法,可以稱為非虛方法,其余情況稱為虛方法,不過有一個特例,即被final關(guān)鍵字修飾的方法,雖然使用invokevirtual指令調(diào)用,由于它無法被覆蓋重寫,所以也是一種非虛方法。
非虛方法的調(diào)用是一個靜態(tài)的過程,由于目標(biāo)方法只有一個確定的版本,所以在類加載的解析階段就可以把符合引用解析為直接引用,而虛方法的調(diào)用是一個分派的過程,有靜態(tài)也有動態(tài),可分為靜態(tài)單分派、靜態(tài)多分派、動態(tài)單分派和動態(tài)多分派。
靜態(tài)分派
靜態(tài)分派發(fā)生在代碼的編譯階段。
public class StaticDispatch {static abstract class Humnan {}static class Man extends Humnan {}static class Woman extends Humnan {}public void hello(Humnan guy) {System.out.println("hello, Humnan");}public void hello(Man guy) {System.out.println("hello, Man");}public void hello(Woman guy) {System.out.println("hello, Woman");}public static void main(String[] args) {Humnan man = new Man();Humnan woman = new Woman();StaticDispatch dispatch = new StaticDispatch();dispatch.hello(man);dispatch.hello(woman);} }運(yùn)行結(jié)果:
hello, Humnan hello, Humnan相信有經(jīng)驗的同學(xué)看完代碼后就能得出正確的結(jié)果,但為什么會這樣呢?先看看main方法的字節(jié)碼指令
通過字節(jié)碼指令,可以發(fā)現(xiàn)兩次hello方法都是通過invokevirtual指令進(jìn)行調(diào)用,而且調(diào)用的是參數(shù)為Human類型的hello方法。
Humnan man = new Man();上述代碼中,變量man擁有兩個類型,一個靜態(tài)類型Human,一個實際類型Man,靜態(tài)類型在編譯期間可知。
在編譯階段,Java編譯器會根據(jù)參數(shù)的靜態(tài)類型決定調(diào)用哪個重載版本,但在有些情況下,重載的版本不是唯一的,這樣只能選擇一個“更加合適的版本”進(jìn)行調(diào)用,所以不建議在實際項目中使用這種模糊的方法重載。
動態(tài)分派
在運(yùn)行期間根據(jù)參數(shù)的實際類型確定方法執(zhí)行版本的過程稱為動態(tài)分派,動態(tài)分派和多態(tài)性中的重寫(override)有著緊密的聯(lián)系。
public class DynamicDispatch {static abstract class Humnan {abstract void say();}static class Man extends Humnan {@Overridevoid say() {System.out.println("hello, i'm Man");}}static class Woman extends Humnan {@Overridevoid say() {System.out.println("hello, i'm Woman");}}public static void main(String[] args) {Humnan man = new Man();Humnan woman = new Woman();man.say();woman.say();} }運(yùn)行結(jié)果:
hello, i'm Man hello, i'm Woman對于習(xí)慣了面向?qū)ο笏季S的同學(xué)對于這個結(jié)果應(yīng)該是理所當(dāng)然的。這種情況下,顯然不能再根據(jù)靜態(tài)類型來決定方法的調(diào)用了,導(dǎo)致不同輸出結(jié)果的原因很簡單,man和woman的實際類型不同,但是JVM如何根據(jù)實際類型決定需要調(diào)用哪個方法?
main方法的字節(jié)碼指令
1.字節(jié)碼0 ~ 15行對應(yīng)以下代碼:
Humnan man = new Man(); Humnan woman = new Woman();在Java堆上申請內(nèi)存空間和實例化對象,并將這兩個實例的引用分別存放到局部變量表的第1、2位置的Slot中。
2.字節(jié)碼16~21行對應(yīng)以下代碼:
man.say(); woman.say();16和20行指令分別把之前存放到局部變量表1、2位置的對象引用壓入操作數(shù)棧的棧頂,這兩個對象是執(zhí)行say方法的接收者(Receiver),17和21行指令進(jìn)行方法調(diào)用。
可以發(fā)現(xiàn),17和21兩條指令完全一樣,但最終執(zhí)行的目標(biāo)方法卻不相同,這得從invokevirtual指令的多態(tài)查找說起了,invokevirtual指令在運(yùn)行時分為以下幾個步驟:
所以上述兩次invokevirtual指令將相同的符號引用解析成了不同對象的直接引用,這個過程就是Java語言中重寫的本質(zhì)。
JVM動態(tài)分派實現(xiàn)
由于動態(tài)分派是非常頻繁的動作,因此在虛擬機(jī)的實際實現(xiàn)中,會基于性能的考慮,并不會如此頻繁的搜索對應(yīng)方法,一般會在方法區(qū)中建立一個虛方法表,使用虛方法表代替方法查詢以提高性能。
虛方法表在類加載的連接階段進(jìn)行初始化,存放著各個方法的實際入口地址,如果某個方法在子類中沒有被重寫,那么子類的虛方法表中該方法的入口地址和父類保持一致。
abstract class Humnan {abstract void say();void run() {System.out.println("Human is run");} } class Man extends Humnan {@Overridevoid say() {System.out.println("hello, i'm Man");}@Overridevoid run() {System.out.println("Man is run");} } class Woman extends Humnan {@Overridevoid say() {System.out.println("hello, i'm Humnan");} }對應(yīng)的虛方法表結(jié)構(gòu)
由于在Woman類中沒有重寫run方法,因此在Woman的虛方法表中,run方法直接指向Human實例。
總結(jié)
- 上一篇: JVM的类加载说明
- 下一篇: Chrome的vimium插件的使用笔记