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