您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關(guān)如何進行JVM方法重載和方法重寫原理分析,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關(guān)知識有一定的了解。
JVM執(zhí)行字節(jié)碼指令是基于棧的架構(gòu),就是說所有的操作數(shù)都必須先入棧,然后再根據(jù)需要出棧進行操作計算,再把結(jié)果進行入棧,這個流程和基于寄存器的架構(gòu)是有本質(zhì)區(qū)別的,而基于寄存器架構(gòu)來實現(xiàn),在不同的機器上可能會無法做到完全兼容,這也是Java會選擇基于棧的設計的原因之一。
我們思考下,當我們調(diào)用一個方法時,參數(shù)是怎么傳遞的,返回值又是怎么保存的,一個方法調(diào)用之后又是如何繼續(xù)下一個方法調(diào)用的呢?調(diào)用過程中肯定會存儲一些方法的參數(shù)和返回值等信息,這些信息存儲在哪里呢?
JVM系列文章1中我們提到了,每次調(diào)用一個方法就會產(chǎn)生一個棧幀,所以我們肯定可以想到棧幀就存儲了所有調(diào)用過程中需要使用到的數(shù)據(jù)?,F(xiàn)在就讓我們深入的去了解一下Java虛擬機棧中的棧幀吧。
當我們調(diào)用一個方法的時候,就會產(chǎn)生一個棧幀,當一個方法調(diào)用完成時,它所對應的棧幀將被銷毀,無論這種完成是正常的還是突然的(拋出一個未捕獲的異常)。
每個棧幀中包括局部變量表(Local Variables)、操作數(shù)棧(Operand Stack)、動態(tài)鏈接(Dynamic Linking)、方法返回地址(Return Address)和額外的附加信息。
在給定的線程當中,永遠只有一個棧幀是活動的,所以活動的棧幀又稱之為當前棧幀,而其對應的方法則稱之為當前方法,定義了當前方法的類則稱之為當前類。當一個方法調(diào)用結(jié)束時,其對應的棧幀也會被丟棄。
局部變量表是以數(shù)組的形式存儲的,而且當前棧幀的方法所需要分配的最大長度是在編譯時就確定了。局部變量表通過index來尋址,變量從index[0]開始傳遞。
局部變量表的數(shù)組中,每一個位置可以保存一個32位的數(shù)據(jù)類型:boolean、byte、char、short、int、float、reference或returnAddress類型的值。而對于64位的數(shù)據(jù)類型long和double則需要兩個位置來存儲,但是因為局部變量表是屬于線程私有的,所以雖然被分割為2個變量存儲,依然不用擔心會出現(xiàn)安全性問題。
對于64位的數(shù)據(jù)類型,假如其占用了數(shù)組中的index[n]和index[n+1]兩個位置,那么不允許單獨訪問其中的某一個位置,Java虛擬機規(guī)范中規(guī)定,如果出現(xiàn)一個64位的數(shù)據(jù)被單獨訪問某一部分時,則在類加載機制中的校驗階段就應該拋出異常。
Java虛擬機在方法調(diào)用時使用局部變量進行傳遞參數(shù)。在類方法(static方法)調(diào)用中,所有參數(shù)都以從局部變量中的index[0]開始進行參數(shù)傳遞。而在實例方法調(diào)用上,index[0]固定用來傳遞方法所屬于的對象實例,其余所有參數(shù)則在從局部變量表內(nèi)index[1]的位置開始進行傳遞。
注意:局部變量表中的變量不可以直接使用,如需要使用的話,必須通過相關(guān)指令將其加載至操作數(shù)棧中作為操作數(shù)才能使用
操作數(shù)棧,在上下文語義清晰時,也可以稱之為操作棧(Operand Stack),是一個后進先出(Last In First Out,LIFO)棧,同局部變量表一樣,操作數(shù)棧的最大深度也是在編譯時就確定的。
操作數(shù)棧在剛被創(chuàng)建時(也就是方法剛被執(zhí)行的時候)是空的,然后在執(zhí)行方法的過程中,通過虛擬機指令將常量/值從局部變量表或字段加載到操作數(shù)棧中,然后對其進行操作,并將操作結(jié)果壓入棧內(nèi)。
操作數(shù)堆棧上的每個條目都可以保存任何Java虛擬機類型的值,包括long或double類型的值。
注意:我們必須以適合其類型的方式對操作數(shù)堆棧中的值進行操作。例如,不可能將兩個int類型的值壓入棧后將其視為long類型,也不可能將兩個float類型值壓入棧內(nèi)后使用iadd指令將其添加。
每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接。
在Class文件中的常量池中存有大量的符號引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用作為參數(shù),這些符號引用一部分會在類加載階段或者第一次使用的時候就轉(zhuǎn)化為直接引用,這種就稱為靜態(tài)解析。而另外一部分則會在每一次運行期間才會轉(zhuǎn)化為直接引用,這部分就稱為動態(tài)連接。
當一個方法開始執(zhí)行后,只有兩種方式可以退出:一種是遇到方法返回的字節(jié)碼指令;一種是遇見異常,并且這個異常沒有在方法體內(nèi)得到處理。
如果對當前方法的調(diào)用正常完成,則可能會向調(diào)用方法返回一個值。當被調(diào)用的方法執(zhí)行其中一個返回指令時,返回指令的選擇必須與被返回值的類型相匹配(如果有的話)。
方法正常退出時,當前棧幀通過將調(diào)用者的pc程序計數(shù)器適當?shù)牟⑻^當前的調(diào)用指令來恢復調(diào)用程序的狀態(tài),包括它的局部變量表和操作數(shù)堆棧。然后繼續(xù)在調(diào)用方法的棧幀來執(zhí)行后續(xù)流程,如果有返回值的話則需要將返回值壓入操作數(shù)棧。
如果在方法中執(zhí)行Java虛擬機指令導致Java虛擬機拋出異常,并且該異常沒有在方法中處理,那么方法調(diào)用會突然結(jié)束,因為異常導致的方法突然結(jié)束永遠不會有返回值返回給它的調(diào)用者。
這一部分具體要看虛擬機產(chǎn)商是如何實現(xiàn)的,虛擬機規(guī)范并沒有對這部分進行描述。
上面的概念聽起來有點抽象,下面我們就通過一個簡單的例子來演示一下方法的執(zhí)行流程。
package com.zwx.jvm; public class JVMDemo { public static void main(String[] args) { int sum = add(1, 2); print(sum); } public static int add(int a, int b) { a = 3; int result = a + b; return result; } public static void print(int num) { System.out.println(num); } }
要想了解Java虛擬機的執(zhí)行流程,那么我們必須要對類進行編譯,得到字節(jié)碼文件,執(zhí)行如下命令
javap -c xxx\xxx\JVMDemo.class >1.txt
將JVMDemo.class生成的字節(jié)碼指令輸出到1.txt文件中,然后打開,看到如下字節(jié)碼指令:
Compiled from "JVMDemo.java" public class com.zwx.jvm.JVMDemo { public com.zwx.jvm.JVMDemo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_1 1: iconst_2 2: invokestatic #2 // Method add:(II)I 5: istore_1 6: iload_1 7: invokestatic #3 // Method print:(I)V 10: return public static int add(int, int); Code: 0: iconst_3 1: istore_0 2: iload_0 3: iload_1 4: iadd 5: istore_2 6: iload_2 7: ireturn public static void print(int); Code: 0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 3: iload_0 4: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 7: return }
如果是第一次接觸可能指令看不太懂,但是大致的類結(jié)構(gòu)還是很清晰的,我們先來對用到的字節(jié)碼指令大致說明一下:
iconst_i
表示將整型數(shù)字i壓入操作數(shù)棧,注意,這里i的返回只有-1~5,如果不在這個范圍會采用其他指令,如當int取值范圍是[-128,127]時,會采用bipush指令。
invokestatic
表示調(diào)用一個靜態(tài)方法
istore_n
這里表示將一個整型數(shù)字存入局部變量表的索引n位置,因為局部變量表是通過一個數(shù)組形式來存儲變量的
iload_n
表示將局部變量位置n的變量壓入操作數(shù)棧
ireturn
將當前方法的結(jié)果返回到上一個棧幀
invokevirtual
調(diào)用虛方法
了解了字節(jié)碼指令的大概意思,接下來就讓我們來演示一下主要的幾個執(zhí)行流程:
1、代碼編譯之后大致得到如下的一個Java虛擬機棧,注意這時候操作數(shù)棧都是空的(pc寄存器的值在這里暫不考慮 ,實際上調(diào)用指令的過程,pc寄存器是會一直發(fā)生變化的)
2、執(zhí)行iconst_1和iconst_2兩個指令,也就是從本地變量中把整型1和2兩個數(shù)字壓入操作數(shù)棧內(nèi):
4、add棧幀中調(diào)用iconst_3指令,從本地變量中將整型3壓入操作數(shù)棧
6、調(diào)用iload_0和iload_1,將局部變量表中index[0]和index[1]兩個位置的變量壓入操作數(shù)棧
8、執(zhí)行istore_2指令,將當前棧頂元素彈出存入局部變量表index[2]的位置,并再次調(diào)用iload_2從局部變量表內(nèi)將index[2]位置的數(shù)據(jù)壓入操作數(shù)棧內(nèi)
9、最后執(zhí)行ireturn命令將結(jié)果5返回main棧幀,此時棧幀add被銷毀,回到main棧幀繼續(xù)后續(xù)執(zhí)行
方法的調(diào)用大致就是不斷的入棧和出棧的過程,上述的過程省略了很多細節(jié),只關(guān)注了大致流程即可,實際調(diào)用比圖中要復雜的多。
我們知道,Java是一種面向?qū)ο笳Z言,支持多態(tài),而多態(tài)的體現(xiàn)形式就是方法重載和方法重寫,那么Java虛擬機又是如何確認我們應該調(diào)用哪一個方法的呢?
首先,我們來看一下方法的字節(jié)碼調(diào)用指令,在Java中,提供了4種字節(jié)碼指令來調(diào)用方法(jdk1.7之前):
1、invokestatic:調(diào)用靜態(tài)方法
2、invokespecial:調(diào)用實例構(gòu)造器方法,私有方法,父類方法
3、invokevirtual:調(diào)用所有的虛方法
4、invokeinterface:調(diào)用接口方法(運行時會確定一個實現(xiàn)了接口的對象)
注意:在JDK1.7開始,Java新增了一個指令invokedynamic,這個是為了實現(xiàn)“動態(tài)類型語言”而引入的,在這里我們暫不討論
在類加載機制中的解析階段,主要做的事情就是將符號引用轉(zhuǎn)為直接引用,但是,對方法的調(diào)用而言,有一個前提,那就是在方法真正運行之前就可以唯一確定具體要調(diào)用哪一個方法,而且這個方法在運行期間是不可變的。只有滿足這個前提的方法才會在解析階段直接被替換為直接引用,否則只能等到運行時才能最終確定。
在Java語言中,滿足“編譯器可知,運行期不可變”這個前提的方法,被稱之為非虛方法。非虛方法在類加載機制中的解析階段就可以直接將符號引用轉(zhuǎn)化為直接引用。非虛方法有4種:
1、靜態(tài)方法
2、私有方法
3、實例構(gòu)造器方法
4、父類方法(通過super.xxx調(diào)用,因為Java是單繼承,只有一個父類,所以可以確定方法的唯一)
除了非虛方法之外的非final方法就被稱之為虛方法,虛方法需要運行時才能確定真正調(diào)用哪一個方法。Java語言規(guī)范中明確指出,final方法是一種非虛方法,但是final又屬于比較特殊的存在,因為final方法和其他非虛方法調(diào)用的字節(jié)碼指令不一樣。
知道了虛方法的類型,再結(jié)合上面的方法的調(diào)用指令,我們可以知道,虛方法就是通過字節(jié)碼指令invokestatic和invokespecial調(diào)用的,而final方法又是一個例外,final方法是通過字節(jié)碼指令invokevirtual調(diào)用的,但是因為final方法的特性就是不可被重寫,無法覆蓋,所以必然是唯一的,雖然調(diào)用指令不同,但是依然屬于非虛方法的范疇。
先來看一個方法重載的例子:
package com.zwx.jvm.overload; public class OverloadDemo { static class Human { } static class Man extends Human { } static class WoMan extends Human { } public void hello(Human human) { System.out.println("Hi,Human"); } public void hello(Man man) { System.out.println("Hi,Man"); } public void hello(WoMan woMan) { System.out.println("Hi,Women"); } public static void main(String[] args) { OverloadDemo overloadDemo = new OverloadDemo(); Human man = new Man(); Human woman = new WoMan(); overloadDemo.hello(man); overloadDemo.hello(woman); } }
輸出結(jié)果為:
Hi,Human Hi,Human
這里,Java虛擬機為什么會選擇參數(shù)為Human的方法來進行調(diào)用呢?
在解釋這個問題之前,我們先來介紹一個概念:宗量
方法的接收者(調(diào)用者)和方法參數(shù)統(tǒng)稱為宗量。而最終決定方法的分派就是基于宗量來選擇的,故而根據(jù)基于多少種宗量來選擇方法又可以分為:
單分派:根據(jù)1個宗量對方法進行選擇
多分派:根據(jù)1個以上的宗量對方法進行選擇
知道了方法的分派是基于宗量來進行的,那我們再回到上面的例子中就很好理解了。
overloadDemo.hello(man);
這句代碼中overloadDemo表示接收者,man表示參數(shù),而接收者是確定唯一的,就是overloadDemo實例,所以決定調(diào)用哪個方法的只有參數(shù)(包括參數(shù)類型和個數(shù)和順序)這一個宗量。我們再看看參數(shù)類型:
Human man = new Man();
這句話中,Human稱之為變量的靜態(tài)類型,而Man則稱之為變量的實際類型,而Java虛擬機在確認重載方法時是基于參數(shù)的靜態(tài)類型來作為判斷依據(jù)的,故而最終實際上不管你右邊new的對象是哪個,調(diào)用的都是參數(shù)類型為Human的方法。
所有依賴變量的靜態(tài)類型來定位方法執(zhí)行的分派動作就稱之為靜態(tài)分派。靜態(tài)分派最典型的應用就是方法重載。
方法重載在編譯期就能確定方法的唯一,不過雖然如此,但是在有些情況下,這個重載版本不是唯一的,甚至是有點模糊的。產(chǎn)生這個原因就是因為字面量并不需要定義,所以字面量就沒有今天類型,比如我們直接調(diào)用一個方法:xxx.xxx(‘1’),這個字面量1就是模糊的,并沒有對應靜態(tài)類型。我們再來看一個例子:
package com.zwx.jvm.overload; import java.io.Serializable; public class OverloadDemo2 { public static void hello(Object a){ System.out.println("Hello,Object"); } public static void hello(double a){ System.out.println("Hello,double"); } public static void hello(Double a){ System.out.println("Hello,Double"); } public static void hello(float a){ System.out.println("Hello,float"); } public static void hello(long a){ System.out.println("Hello,long"); } public static void hello(int a){ System.out.println("Hello,int"); } public static void hello(Character a){ System.out.println("Hello,Character"); } public static void hello(char a){ System.out.println("Hello,char"); } public static void hello(char ...a){ System.out.println("Hello,chars"); } public static void hello(Serializable a){ System.out.println("Hello,Serializable"); } public static void main(String[] args) { OverloadDemo2.hello('1'); } }
這里的輸出結(jié)果是
Hello,char
然后如果把該方法注釋掉,就會輸出:
Hello,int
再把int方法注釋掉,那么會依次按照如下順序進行方法調(diào)用輸出:
char->int->long->float->double->Character->Serializable->Object->chars
可以看到,多參數(shù)的優(yōu)先級最低,之所以會輸出Serializable是因為包裝類Character實現(xiàn)了Serializable接口,注意示例中double的包裝類Double,并不會被執(zhí)行。
我們把上面第1個例子修改一下:
package com.zwx.jvm.override; public class OverrideDemo { static class Human { public void hello(Human human) { System.out.println("Hi,Human"); } } static class Man extends Human { @Override public void hello(Human human) { System.out.println("Hi,Man"); } } static class WoMan extends Human { @Override public void hello(Human human) { System.out.println("Hi,Women"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new WoMan(); man.hello(man); man.hello(woman); woman.hello(woman); woman.hello(man); } }復制代碼
輸出結(jié)果為:
Hi,Man Hi,Man Hi,Women Hi,Women復制代碼
這里靜態(tài)類型都是Human,但是卻輸出了兩種結(jié)果,所以肯定不是按照靜態(tài)類型來分派方法了,而從結(jié)果來看應該是按照了調(diào)用者的實際類型來進行的判斷。
執(zhí)行javap命令把類轉(zhuǎn)換成字節(jié)碼:
Compiled from "OverrideDemo.java" public class com.zwx.jvm.override.OverrideDemo { public com.zwx.jvm.override.OverrideDemo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class com/zwx/jvm/override/OverrideDemo$Man 3: dup 4: invokespecial #3 // Method com/zwx/jvm/override/OverrideDemo$Man."<init>":()V 7: astore_1 8: new #4 // class com/zwx/jvm/override/OverrideDemo$WoMan 11: dup 12: invokespecial #5 // Method com/zwx/jvm/override/OverrideDemo$WoMan."<init>":()V 15: astore_2 16: aload_1 17: aload_1 18: invokevirtual #6 // Method com/zwx/jvm/override/OverrideDemo$Human.hello:(Lcom/zwx/jvm/override/OverrideDemo$Human;)V 21: aload_1 22: aload_2 23: invokevirtual #6 // Method com/zwx/jvm/override/OverrideDemo$Human.hello:(Lcom/zwx/jvm/override/OverrideDemo$Human;)V 26: aload_2 27: aload_2 28: invokevirtual #6 // Method com/zwx/jvm/override/OverrideDemo$Human.hello:(Lcom/zwx/jvm/override/OverrideDemo$Human;)V 31: aload_2 32: aload_1 33: invokevirtual #6 // Method com/zwx/jvm/override/OverrideDemo$Human.hello:(Lcom/zwx/jvm/override/OverrideDemo$Human;)V 36: return }
我們可以發(fā)現(xiàn)這里的方法調(diào)用使用了指令invokevirtual來調(diào)用,因為根據(jù)上面的分類可以判斷,hello方法均是虛方法。
main方法大概解釋一下,
main方法中,第7行(Code列序號)和第15行是分別把Man對象實例和Women對象實例存入局部變量變的index[1]和index[2]兩個位置,然后16,17兩行,21,22兩行,26,27兩行,31,32兩行分別是把需要用到的方法調(diào)用者和參數(shù)壓入操作數(shù)棧,然后調(diào)用invokevirtual指令調(diào)用方法
。
所以上面最關(guān)鍵的就是invokevirtual指令到底是如何工作的呢?invokevirtual主要是按照如下步驟進行方法選擇的:
1、找到當前操作數(shù)棧中的方法接收者(調(diào)用者),記下來,比如叫Caller
2、然后在類型Caller中去找方法,如果找到方法簽名一致的方法,則停止搜索,開始對方法校驗,校驗通過直接調(diào)用,校驗不通過,直接拋IllegalAccessError異常
3、如果在Caller中沒有找到方法簽名一致的方法,則往上找父類,以此類推,直到找到為止,如果到頂了還沒找到匹配的方法,則拋出AbstractMethodError異常
上面的方法重寫例子中,在運行期間才能根據(jù)實際類型來確定方法的執(zhí)行版本的分派過程就稱之為動態(tài)分派。
上面方法重載的第1個示例中,是一個靜態(tài)分派過程,靜態(tài)分配過程中Java虛擬機選擇目標方法有兩點:
1、靜態(tài)類型
2、方法參數(shù)
也就是用到了2個宗量來進行分派,所以是一個靜態(tài)多分派的過程。
而上面方法重寫的例子中,因為方法簽名是固定的,也就是參數(shù)是固定的,那么就只有一個宗量-靜態(tài)類型,能最終確定方法的調(diào)用,所以屬于動態(tài)單分派。
所以可以得出對Java而言:Java是一門靜態(tài)多分派,動態(tài)單分派語言
關(guān)于如何進行JVM方法重載和方法重寫原理分析就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。