java符號(hào)的定位與解析,從本質(zhì)上來看,就是一個(gè)消除歧義的過程躲惰,這個(gè)過程從編譯到運(yùn)行励七,橫跨了多個(gè)階段智袭,本文主要目的在于劃分清楚哪個(gè)階段消除了什么歧義
1.符號(hào)的起點(diǎn)-java代碼
public class Parent {
public void sayHi(){
}
public static void main(String[] args) {
new Parent().sayHi("hi");
}
}
那么從java語義上看,表達(dá)的意思大概就是:
1掠抬、有個(gè)public的Parent類
2吼野、這個(gè)類有個(gè)類方法叫sayHi
3、類方法sayHi是個(gè)沒有參數(shù)也沒有返回值的public方法
4两波、main函數(shù)調(diào)用了sayHi函數(shù)
如同你畫我猜一般瞳步,如何將這些意思精確的傳遞給jvm直到最終程序的運(yùn)行呢
2.編譯后-字節(jié)碼
編譯的過程就是將某一種語言編寫的程序翻譯成為一個(gè)等價(jià)的、用另一種語言編寫的程序雨女。那么對于java而言谚攒,編譯就是將java代碼翻譯成字節(jié)碼阳准,編譯的細(xì)節(jié)就不分析了氛堕,主要看編譯的結(jié)果--字節(jié)碼,以下是截取的部分字節(jié)碼野蝇,分為兩塊讼稚,一塊是方法表,一塊是常量池
常量池中有一個(gè)常量指向sayHi函數(shù):
Constant pool:
#..."省略"
#4 = Methodref #2.#17 // Parent.sayHi:()V
Parent.sayHi:()V就是用來描述sayHi函數(shù)的符號(hào)引用绕沈,描述的很精確:類Parent的锐想,沒有入?yún)⒌模祷豽oid的乍狐,名為sayHi的方法
那么main方法里怎么調(diào)用這個(gè)sayHi()的呢赠摇,看方法表:
public static void main(java.lang.String[]);
...
7: invokevirtual #4 // Method sayHi:()V
...
invokevirtual可以暫時(shí)理解成調(diào)用,#4當(dāng)然就是上文中常量池中的常量#4浅蚪,它指向sayHi()方法藕帜,如此一個(gè)調(diào)用函數(shù)的過程就被字節(jié)碼描述清楚了
從這個(gè)簡單的例子可以得出結(jié)論:
字節(jié)碼中的符號(hào)引用僅僅是一個(gè)描述性的符號(hào)(如:Parent.sayHi:()V),并沒有保存最終的內(nèi)存布局信息惜傲,這點(diǎn)與C語言的編譯結(jié)果不一樣
3.編譯期間解決的歧義
3.1 重載
假如一個(gè)類中洽故,void sayHi(String string)
和 void sayHi(Object object)
兩個(gè)方法構(gòu)成重載,當(dāng)調(diào)用 sayHi("hi")時(shí)盗誊,看看字節(jié)碼中的指令是什么:
invokevirtual #9 // Method sayHi:(Ljava/lang/String;)V
顯然时甚,#9常量指向的是void sayHi(String string)
方法,那么可以得出結(jié)論:
編譯期間哈踱,重載造成的歧義就被解決了荒适,在字節(jié)碼中,已經(jīng)選出應(yīng)該調(diào)用的方法
繼續(xù)看這個(gè)例子开镣,調(diào)用方法時(shí)傳入的參數(shù)為"hi",既是String類型刀诬,也是Object類型,但是在字節(jié)碼中哑子,直接認(rèn)定了調(diào)用(Ljava/lang/String;)V
舅列,這是為什么呢肌割,以下是《深入理解jvm》書中給出的解釋:
Java 編譯器選取重載方法的過程共分為三個(gè)階段:
1、在不考慮對基本類型自動(dòng)裝拆箱(auto-boxing帐要,auto-unboxing)把敞,以及可變長參數(shù)的情況下選取重載方法;
2榨惠、如果在第 1 個(gè)階段中沒有找到適配的方法奋早,那么在允許自動(dòng)裝拆箱,但不允許可變長參數(shù)的情況下選取重載方法赠橙;
3耽装、如果在第 2 個(gè)階段中沒有找到適配的方法,那么在允許自動(dòng)裝拆箱以及可變長參數(shù)的情況下選取重載方法期揪。
如果 Java 編譯器在同一個(gè)階段中找到了多個(gè)適配的方法掉奄,那么它會(huì)在其中選擇一個(gè)最為貼切的,而決定貼切程度的一個(gè)關(guān)鍵就是形式參數(shù)類型的繼承關(guān)系凤薛。
3.2 遮蔽
下面這段代碼姓建,在init()函數(shù)中定義了一個(gè)與類變量同名的局部變量a
public class Parent5 {
public String a = "out";
public void init(){
String a = "in";
System.out.println(a);
System.out.println(this.a);
}
public static void main(String[] args){
new Parent5().init();
}
}
直接看字節(jié)碼中是如何獲取a和this.a對應(yīng)的值的:
常量池:
Constant pool:
#3 = Fieldref #7.#24 // Parent5.a:Ljava/lang/String;
#4 = String #25 // in
獲取a的值
0: ldc #4 // String in
從常量池的#4常量中獲取值(指向了字符串"in"
)
獲取this.a的值
getfield #3 // Field a:Ljava/lang/String;
從常量池的#3常量中獲取值(指向了Parent5.a:Ljava/lang/String
,即類變量a)
顯然,字節(jié)碼層面上已經(jīng)對調(diào)用哪個(gè)變量區(qū)分的明明白白缤苫,那么可以得出結(jié)論:
java語言層面上定義的遮蔽速兔,其在編譯過程中就被實(shí)現(xiàn)
3.3 隱藏
隱藏的典型場景是對于一個(gè)靜態(tài)方法,多態(tài)的特性失效了
java代碼:
public class Parent4 {
public static void sayHi(){
System.out.println("hi,son");
}
}
class Son4 extends Parent4{
public static void sayHi(){
System.out.println("hi,parent");
}
public static void main(String[] args) {
Parent4 son4 = new Son4();
son4.sayHi();
}
}
本例中活玲,將打印出"hi,son"涣狗,這與多態(tài)的場景基本一致,區(qū)別在于本例中的方法是靜態(tài)方法舒憾,所以盡管真正的實(shí)例是Son4類型的镀钓,但最后還是調(diào)用的父類的方法
son4.sayHi();對應(yīng)的字節(jié)碼:
10: invokestatic #7 // Method Parent4.sayHi:()V
上述指令中,#7常量指向的是父類方法(Method Parent4.sayHi:()V
)珍剑,且使用的是invokestatic
指令掸宛,這條指令是專門用來調(diào)用靜態(tài)方法的,具體invokestatic的實(shí)現(xiàn)邏輯可以看源碼解析
簡單總結(jié)一下就是: 在解析invokestatic #7時(shí)招拙,直接將#7指向的符號(hào)引用解析成直接引用并返回唧瘾,并不關(guān)心運(yùn)行時(shí)實(shí)際對象的類型(即只有連接時(shí)解析,沒有運(yùn)行時(shí)解析别凤,后文會(huì)進(jìn)行介紹)
那么可以得出結(jié)論:
java語言層面上定義的隱藏饰序,是在編譯時(shí)消除的歧義
4.解析期間解決的歧義
上文講到,在字節(jié)碼中规哪,調(diào)用方法時(shí)求豫,指向的只是一個(gè)方法的符號(hào)引用,而不是最終的內(nèi)存地址,那么就需要一個(gè)步驟將符號(hào)引用轉(zhuǎn)為直接引用蝠嘉,這一步驟稱為解析最疆,以下是一些基本的名詞解釋
符號(hào)引用:是以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量蚤告,只要使用時(shí)能無歧義地定位到目標(biāo)即可. 符號(hào)引用的目標(biāo)不一定要加載到內(nèi)存中.
直接引用:是直接指向目標(biāo)的指針努酸、相對偏移量或是一個(gè)能間接定位到目標(biāo)的句柄. 如果有了直接引用,那引用的目標(biāo)必定存在于內(nèi)存中.
虛擬機(jī)規(guī)范中并沒有明確規(guī)定解析階段發(fā)生的具體時(shí)間杜恰,只要求了在執(zhí)行 anewarray获诈、checkcast、getfield心褐、getstatic舔涎、instanceof、invokedynamic逗爹、invokeinterface亡嫌、invokespecial、invokestatic桶至、invokevirtual昼伴、ldc匾旭、ldc_w镣屹、multianewarray、new价涝、putfield女蜈、putstatic 用于操作符號(hào)引用的字節(jié)碼指令前,先對它們所使用的符號(hào)進(jìn)行解析. 所以虛擬機(jī)實(shí)現(xiàn)可以根據(jù)需要來判斷到底是在類被加載時(shí)就對常量池中的符號(hào)進(jìn)行解析色瘩,還是等到一個(gè)符號(hào)將要被使用前才解析它.
4.1 多態(tài)&重寫
以一個(gè)多態(tài)的例子來分析解析的過程
java代碼:
class Son3 extends Parent3
...
Parent3 son3 = new Son3();
son3.sayHi();
son3.sayHi()對應(yīng)的字節(jié)碼:
invokevirtual #7 // Method Parent3.sayHi:()V
可以看出伪窖,#7常量指向的是父類方法(Method Parent3.sayHi:()V
),顯然在編譯后居兆,歧義沒有消除覆山,那么來看解析invokevirtual
指令,可以將其分為兩個(gè)步驟:連接時(shí)解解析 和 運(yùn)行時(shí)解析 (根據(jù)源碼函數(shù)翻譯而來)
4.1.1 連接時(shí)解析(linktime_resolve_virtual_method)
根據(jù)字節(jié)碼中的符號(hào)引用Parent3.sayHi:()V
泥栖,解析其對應(yīng)的直接引用簇宽,具體尋找過程如下(源碼可看另一篇文章):
1、遍歷Parent3類的方法列表吧享,根據(jù)符號(hào)引用來找到匹配的方法魏割,并返回它的直接引用(methodHandle)
2、如果第一步找不到钢颂,就去Parent3的父類中找钞它,還找不到,就去Parent3實(shí)現(xiàn)的接口中找
4.1.2 運(yùn)行時(shí)解析(runtime_resolve_virtual_method)
連接時(shí)解析已經(jīng)獲得了Parent3.sayHi:()V
對應(yīng)的直接引用methodHandle,接下來的步驟是:
1遭垛、根據(jù)methodHandle尼桶,獲取sayHi()函數(shù)在Parent3類對應(yīng)虛方法表中的位置vtable_index
2、jvm在運(yùn)行時(shí)可以獲取到實(shí)際對象的類型锯仪,即Son3類疯汁,獲取其對應(yīng)的虛表
3、從Son3的虛方法表中獲取vtable_index位置指向的直接引用
invokevirtual #7 // Method Parent3.sayHi:()V
解析前卵酪,常量#7指向Parent3.sayHi:()V的符號(hào)引用
解析后幌蚊,常量#7指向Son3::sayHi()V的直接引用
這就是多態(tài)的實(shí)現(xiàn),在運(yùn)行時(shí)解析消除了歧義
解析的過程中設(shè)計(jì)到了虛方法表的概念溃卡,下面會(huì)進(jìn)行介紹
4.1.3 虛方法表
虛方法表(簡稱虛表)的具體初始化細(xì)節(jié)和源碼可以看另一篇文章溢豆,這里總結(jié)一些相關(guān)的概念:
1、虛表是一種用于實(shí)現(xiàn)dynamic dispatch機(jī)制(或者說runtime method binding機(jī)制瘸羡,也就是我們說的多態(tài))的工具漩仙,C++也有用到
2、每個(gè)類對應(yīng)一個(gè)虛表犹赖,虛表中不存放靜態(tài)方法和私有方法(只為invoke_virtual服務(wù))
3队他、虛表中,也會(huì)存放父類的方法峻村,且對于同一個(gè)函數(shù)麸折,在父類和子類的虛表中的位置是一樣的,如果沒有重寫,二者指向同一個(gè)引用粘昨,若發(fā)生重寫垢啼,則不同
舉個(gè)簡單的例子,Son3重寫了Parent3的sayHi方法张肾,以下是它們對應(yīng)的虛表
5. 總結(jié)
1芭析、編譯期間消除了重載、遮蔽吞瞪、隱藏這三種情況的歧義
2馁启、解析是符號(hào)引用轉(zhuǎn)化成直接引用的過程
3、解析可分為連接時(shí)解析和運(yùn)行時(shí)解析兩步芍秆,靜態(tài)方法的解析沒有第二步
4惯疙、多態(tài)&重寫則是在解析時(shí)(通過虛表)消除的歧義