摘要:本文已收錄修煉內(nèi)功躍遷之路我們寫的方法在被編譯為文件后是如何被虛擬機執(zhí)行的對于重寫或者重載的方法,是在編譯階段就確定具體方法的么如果不是,虛擬機在運行時又是如何確定具體方法的方法調(diào)用不等于方法執(zhí)行,一切方法調(diào)用在文件中都只是常量池中的符號引
本文已收錄【修煉內(nèi)功】躍遷之路
『我們寫的Java方法在被編譯為class文件后是如何被虛擬機執(zhí)行的?對于重寫或者重載的方法,是在編譯階段就確定具體方法的么?如果不是,虛擬機在運行時又是如何確定具體方法的?』
方法調(diào)用不等于方法執(zhí)行,一切方法調(diào)用在class文件中都只是常量池中的符號引用,這需要在類加載的解析階段甚至到運行期間才能將符號引用轉(zhuǎn)為直接引用,確定目標方法進行執(zhí)行
在編譯過程中編譯器并不知道目標方法的具體內(nèi)存地址,因此編譯器會暫時使用符號引用來表示該目標方法
編譯代碼
public class MethodDescriptor { public void printHello() { System.out.println("Hello"); } public void printHello(String name) { System.out.println("Hello " + name); } public static void main(String[] args) { MethodDescriptor md = new MethodDescriptor(); md.printHello(); md.printHello("manerfan"); } }
查看其字節(jié)碼
main方法中調(diào)用兩次不同的printHello方法,對應(yīng)class文件中均為invokevirtual指令,分別調(diào)用常量池中的#12及#14,查看常量池
#12及#14對應(yīng)兩個Methodref方法引用,這兩個方法引用均為符號引用(使用方法描述符)而并非直接引用
虛擬機識別方法的關(guān)鍵在于類名、方法名及方法描述符(method descriptor),方法描述符由方法的參數(shù)類型及返回類型構(gòu)成
方法名及方法描述符在編譯階段便可以確定,但對于實際類名,一些場景下(如類繼承)只有在運行時才可知
方法調(diào)用指令目前Java虛擬機里提供了5中方法調(diào)用的字節(jié)碼指令
invokestatic: 調(diào)用靜態(tài)方法
invokespecial: 調(diào)用實例構(gòu)造器
invokevirtual: 調(diào)用虛方法(會在運行時確定具體的方法對象)
invokeinterface: 調(diào)用接口方法(會在運行時確定一個實現(xiàn)此接口的對象)
invokedynamic: 先在運行時動態(tài)解析出調(diào)用點限定符所引用的方法,然后再執(zhí)行該方法
invokestatic及invokespecial調(diào)用的方法(靜態(tài)方法、構(gòu)造方法、私有方法、父類方法),均可以在類加載的解析階段確定唯一的調(diào)用版本,從而將符號引用直接解析為該方法的直接引用,這些方法稱之為非虛方法
而invokevirtual及invokeinterface調(diào)用的方法(final方法除外,下文提到),在解析階段并不能唯一確定,只有在運行時才能拿到實際的執(zhí)行類從而確定唯一的調(diào)用版本,此時才可以將符號引用轉(zhuǎn)為直接引用,這些方法稱之為虛方法
invokedynamic比較特殊,多帶帶分析
簡單示意,如下代碼
public interface MethodBase { String getName(); } public class BaseMethod implements MethodBase { @Override public String getName() { return "manerfan"; } public void print() { System.out.println(getName()); } } public class MethodImpl extends BaseMethod { @Override public String getName() { return "maner-fan"; } @Override public void print() { System.out.println("Hello " + getName()); }; public String getSuperName() { return super.getName(); } public static String getDefaultName() { return "default"; } } public class MethodDescriptor { public static void print(BaseMethod baseMethod) { baseMethod.print(); } public static String getName(MethodBase methodBase) { return methodBase.getName(); } public static void main(String[] args) { MethodImpl.getDefaultName(); MethodImpl ml = new MethodImpl(); ml.getSuperName(); getName(ml); print(ml); } }
查看MethodDescriptor的字節(jié)碼
不難發(fā)現(xiàn),接口MethodBase中g(shù)etName方法的調(diào)用均被編譯為invokeinterface指令,子類BaseMethod中print方法的調(diào)用則被便以為invokevirtual執(zhí)行,靜態(tài)方法的調(diào)用被編譯為invokestatic指令,而構(gòu)造函數(shù)調(diào)用則被編譯為invokespecial指令
查看MethodImpl字節(jié)碼
可以看到,父類方法的調(diào)用則被編譯為invokespecial指令
橋接方法在JVM - 類文件結(jié)構(gòu)中有介紹方法的訪問標識,其中有兩條 ACC_BRIDGE(橋接方法) 及 ACC_SYNTHETIC(編譯器生成,不會出現(xiàn)在源碼中),而橋接方法便是由編譯器生成,且會將橋接方法標記為ACC_BRIDGE及ACC_SYNTHETIC,那什么時候會生成橋接方法?
橋接方法是 JDK 1.5 引入泛型后,為了使Java的泛型方法生成的字節(jié)碼和 1.5 版本前的字節(jié)碼相兼容,由編譯器自動生成的,就是說一個子類在繼承(或?qū)崿F(xiàn))一個父類(或接口)的泛型方法時,在子類中明確指定了泛型類型,那么在編譯時編譯器會自動生成橋接方法(當然還有其他情況會生成橋接方法,這里只是列舉了其中一種情況)
public class BaseMethod{ public void print(T obj) { System.out.println("Hello " + obj.toString()); } } public class MethodImpl extends BaseMethod { @Override public void print(String name) { super.print(name); }; }
首先查看BaseMethod字節(jié)碼
由于泛型的擦除機制,print的方法描述符入?yún)⒈粯擞洖?b>(Ljava/lang/Object;)V
再查看MethodImpl字節(jié)碼
MethodImpl只聲明了一個print方法,卻被編譯為兩個,一個方法描述符為(Ljava/lang/String;)V,另一個為(Ljava/lang/Object;)V且標記為ACC_BRIDGE ACC_SYNTHETIC
print(java.lang.Object)方法中做了一層類型轉(zhuǎn)換,將入?yún)⑥D(zhuǎn)為String類型,進而再調(diào)用print(java.lang.String)方法
為什么要生成橋接方法泛型可以保證在編譯階段檢查對象類型是否匹配執(zhí)行的泛型類型,但為了向下兼容(1.5之前),在編譯時則會擦除泛型信息,如果不生成橋接方法則會導(dǎo)致字節(jié)碼中子類方法為print(java.lang.Object)而父類為print(java.lang.String),這樣的情況是無法做到向下兼容的
橋接方法的隱患既然橋接方法是為了向下兼容,那會不會有什么副作用?
public class MethodDescriptor { public static void main(String[] args) { BaseMethod bm = new MethodImpl(); bm.print("manerfan"); bm.print(new Object()); } }
查看字節(jié)碼
可以看到,雖然MethodImpl.print方法入?yún)⒙暶鳛镾tring類型,但實際調(diào)用的還是橋接方法print(java.lang.Object)
由于子類的入?yún)?b>Object,所以編譯并不會失敗,但從MethodImpl的字節(jié)碼中可以看到,橋接方法是有一次類型轉(zhuǎn)換的,在將類型轉(zhuǎn)為String之后會調(diào)用print(java.lang.String)方法,那如果類型轉(zhuǎn)換失敗呢?運行程序可以得到
Hello manerfan Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String at MethodImpl.print(MethodImpl.java:1) at MethodDescriptor.main(MethodDescriptor.java:5)
所以,由于泛型的擦除機制,會導(dǎo)致某些情況下(如方法橋接)的錯誤,只有在運行時才可以被發(fā)現(xiàn)
對于其他情況,大家可以編寫更為具體的代碼查看其字節(jié)碼指令
分派 靜態(tài)分派首先看一個重載的例子
public class StaticDispatch { static abstract class Animal { public abstract void croak(); } static class Dog extends Animal { @Override public void croak() { System.out.println("汪汪叫~"); } } static class Duck extends Animal { @Override public void croak() { System.out.println("呱呱叫~"); } } public void croak(Animal animal) { System.out.println("xx叫~"); } public void croak(Dog dog) { dog.croak(); } public void croak(Duck duck) { duck.croak(); } public static void main(String[] args) { Animal dog = new Dog(); Animal duck = new Duck(); StaticDispatch dispatcher = new StaticDispatch(); dispatcher.croak(dog); dispatcher.croak(duck); } }
運行結(jié)果
xx叫~ xx叫~
起始并不難理解為什么兩次都執(zhí)行了croak(Animal)的方法,這里要區(qū)分變量的靜態(tài)類型以及變量的實際類型
一個對象的靜態(tài)類型在編譯器是可知的,但并不知道其實際類型是什么,實際類型只有在運行時才可知
編譯器在重載時,是通過參數(shù)的靜態(tài)類型(而不是實際類型)作為判定依據(jù)以決定使用哪個重載版本的,所有依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動作成為靜態(tài)分派,靜態(tài)分派發(fā)生在編譯階段,因此嚴格來講靜態(tài)分派并不是虛擬機的行為
動態(tài)分派同樣,還是上述示例,修改main方法
public static void main(String[] args) { Animal dog = new Duck(); Animal duck = new Dog(); dog.croak(); duck.croak(); }
運行結(jié)果
呱呱叫~ 汪汪叫~
顯然這里并不能使用靜態(tài)分派來決定方法的執(zhí)行版本(編譯階段并不知道dog及duck的實際類型),查看字節(jié)碼
兩次croak調(diào)用均使用了invokevirtual指令,invokevirtual指令(invokeinterface類似)運行時解析過程大致為
找到對象實際類型C
在C常量池中查找方法描述符相符的方法,如果找到則返回方法的直接引用,如果無權(quán)訪問則拋jaba.lang.IllegalAccessError異常
如果未找到,則按照繼承關(guān)系從下到上一次對C的各個父類進行第2步的搜索
如果均未找到,則拋java.lang.AbstractMethodError異常
實際運行過程中,動態(tài)分派是非常頻繁的動作,而動態(tài)分派的方法版本選擇需要在類的方法元數(shù)據(jù)中進行搜索,處于性能的考慮,類在方法區(qū)中均會創(chuàng)建一個虛方法表(virtual method table, vtable)及接口方法表(interface method table, itable),使用虛方法表(接口方法表)索引來代替元數(shù)據(jù)查找以提高性能
方法表本質(zhì)上是一個數(shù)組,每個數(shù)組元素都指向一個當前類機器祖先類中非私有的實力方法
動態(tài)調(diào)用在JDK1.7以前,4條方法調(diào)用指令(invokestatic、invokespecial、invokevirtual、invokeinterface),均與包含目標方法類名、方法名及方法描述符的符號引用綁定,invokestatic及invokespecial的分派邏輯在編譯時便確定,invokevirtual及invokeinterface的分配邏輯也由虛擬機在運行時決定,在此之前,JVM虛擬機并不能實現(xiàn)動態(tài)語言的一些特性,典型的例子便是鴨子類型(duck typing)
鴨子類型(duck typing)是多態(tài)(polymorphism)的一種形式,在這種形式中不管對象屬于哪個,也不管聲明的具體接口是什么,只要對象實現(xiàn)了相應(yīng)的方法函數(shù)就可以在對象上執(zhí)行操作
public class StaticDispatch { static class Duck { public void croak() { System.out.println("呱呱叫~"); } } static class Dog { public void croak() { System.out.println("學(xué)鴨子呱呱叫~"); } } public static void duckCroak(Duck duckLike) { duckLike.croak(); } public static void main(String[] args) { Duck duck = new Duck(); Dog dog = new Dog(); duckCroak(duck); duckCroak(dog); // 編譯錯誤 } }
我們不關(guān)心Dog是不是Duck,只要Dog可以像Duck一樣croak就可以
方法句柄Duck Dog croak的問題,我們可以使用反射來解決,也可以使用一種新的、更底層的動態(tài)確定目標方法的機制來實現(xiàn)--方法句柄
方法句柄是一個請類型的、能夠被直接執(zhí)行的引用,類似于C/C++中的函數(shù)指針,可以指向常規(guī)的靜態(tài)方法或者實力方法,也可以指向構(gòu)造器或者字段
public class Dispatch { static class Duck { public void croak() { System.out.println("呱呱叫~"); } } static class Dog { public void croak() { System.out.println("學(xué)鴨子呱呱叫~"); } } public static void duckCroak(MethodHandle duckLike) throws Throwable { duckLike.invokeExact(); } public static void main(String[] args) throws Throwable { Duck duck = new Duck(); Dog dog = new Dog(); MethodType mt = MethodType.methodType(void.class); MethodHandle duckCroak = MethodHandles.lookup().findVirtual(duck.getClass(), "croak", mt).bindTo(duck); MethodHandle dogCroak = MethodHandles.lookup().findVirtual(dog.getClass(), "croak", mt).bindTo(dog); duckCroak(duckCroak); duckCroak(dogCroak); } }
這樣的事情,使用反射不一樣可以實現(xiàn)么?
本質(zhì)上講,Reflection及MethodHandler都是在模擬方法調(diào)用,但Reflection是Java代碼層次的模擬,MethodHandler是字節(jié)碼層次的層次,更為底層
Reflection相比MethodHandler包含更多的信息,Reflection是重量級的,MethodHandler是輕量級的
invokedynamicinvokedynamic是Java1.7引入的一條新指令,用以支持動態(tài)語言的方法調(diào)用,解決原有4條"invoke*"指令方法分派規(guī)則固化在虛擬機中的問題,把如何查找目標方法的決定權(quán)從虛擬機轉(zhuǎn)嫁到具體用戶代碼中,使用戶擁有更高的自由度
invokedynamic將調(diào)用點(CallSite)抽象成一個Java類,并且將原本由Java虛擬機控制的方法調(diào)用以及方法鏈接暴露給了應(yīng)用程序,在運行過程中,每一條invokedynamic指令將捆綁一個調(diào)用點,并且會調(diào)用該調(diào)用點所鏈接的方法句柄
在Java8以前,并不能直接通過Java程序編譯生成invokedynamic指令,這里寫一段代碼用以模擬上述過程
public class DynamicDispatch { /** * 動態(tài)調(diào)用的方法 */ private static void croak(String name) { System.out.println(name + " croak"); } public static void main(String[] args) throws Throwable { INDY_BootstrapMethod().invokeExact("dog"); } /** * 生成啟動方法 */ private static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable { return new ConstantCallSite(lookup.findStatic(DynamicDispatch.class, name, mt)); } /** * 生成啟動方法的MethodType */ private static MethodType MT_BootstrapMethod() { return MethodType.fromMethodDescriptorString( "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)" + "Ljava/lang/invoke/CallSite;", null); } /** * 生成啟動方法的MethodHandle */ private static MethodHandle MH_BootstrapMethod() throws Throwable { return MethodHandles.lookup().findStatic(DynamicDispatch.class, "BootstrapMethod", MT_BootstrapMethod()); } /** * 生成調(diào)用點,動態(tài)調(diào)用 */ private static MethodHandle INDY_BootstrapMethod() throws Throwable { // 生成調(diào)用點 CallSite cs = (CallSite)MH_BootstrapMethod().invokeWithArguments(MethodHandles.lookup(), "croak", MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null)); // 動態(tài)調(diào)用 return cs.dynamicInvoker(); } }
字節(jié)碼中,啟動方法由方法句柄來指定(MH_BootstrapMethod),該句柄指向一個返回類型為調(diào)用點的靜態(tài)方法(BootstrapMethod)
在第一次執(zhí)行invokedynamic時,JVM虛擬機會調(diào)用該指令所對應(yīng)的啟動方法(BootstrapMethod)來生成調(diào)用點
啟動方法(BootstrapMethod)由方法句柄來指定(MH_BootstrapMethod)
啟動方法接受三個固定的參數(shù),分別為 Lookup實例、指代目標方法名的字符串及該調(diào)用點能夠鏈接的方法句柄類型
將調(diào)用點綁定至該invokedynamic指令中,之后的運行中虛擬機會直接調(diào)用綁定的調(diào)用點所鏈接的方法句柄
Lambda表達式Java8中的lambda表達式使用的便是invokedynamic指令
public class DynamicDispatch { public void croak(Suppliername) { System.out.println(name.get() + "croak"); } public static void main(String[] args) throws Throwable { new DynamicDispatch().croak(() -> "dog"); } }
查看字節(jié)碼
可以看到,lambda表達式會被編譯為invokedynamic指令,同時會生成一個私有靜態(tài)方法lambda$main$0,用以實現(xiàn)lambda表達式內(nèi)部的邏輯
其實,除了會生成一個靜態(tài)方法之外,還會額外生成一個內(nèi)部類,lambda啟動方法及調(diào)用點的詳細介紹請轉(zhuǎn) Java8 - Lambda原理-究竟是不是匿名類的語法糖
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/77900.html
摘要:本文已收錄修煉內(nèi)功躍遷之路初次接觸的時候感覺表達式很神奇表達式帶來的編程新思路,但又總感覺它就是匿名類或者內(nèi)部類的語法糖而已,只是語法上更為簡潔罷了,如同以下的代碼匿名類內(nèi)部類編譯后會產(chǎn)生三個文件雖然從使用效果來看,與匿名類或者內(nèi)部類有相 本文已收錄【修煉內(nèi)功】躍遷之路 showImg(https://segmentfault.com/img/bVbui4o?w=800&h=600)...
摘要:本文已收錄修煉內(nèi)功躍遷之路在淺談虛擬機內(nèi)存模型一文中有簡單介紹過,虛擬機棧是線程私有的,每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀,方法執(zhí)行時棧幀入棧,方法結(jié)束時棧幀出棧,虛擬機中棧幀的入棧順序就是方法的調(diào)用順序?qū)懥撕芏辔淖郑疾槐M如意,十分慚 本文已收錄【修煉內(nèi)功】躍遷之路 showImg(https://segmentfault.com/img/bVbtSi5?w=1654&h=96...
摘要:也正是因此,一旦出現(xiàn)內(nèi)存泄漏或溢出問題,如果不了解的內(nèi)存管理原理,那么將會對問題的排查帶來極大的困難。 本文已收錄【修煉內(nèi)功】躍遷之路 showImg(https://segmentfault.com/img/bVbsP9I?w=1024&h=580); 不論做技術(shù)還是做業(yè)務(wù),對于Java開發(fā)人員來講,理解JVM各種原理的重要性不必再多言 對于C/C++而言,可以輕易地操作任意地址的...
摘要:本文已收錄修煉內(nèi)功躍遷之路在誕生之初便提出,各提供商發(fā)布很多不同平臺的虛擬機,這些虛擬機都可以載入并執(zhí)行同平臺無關(guān)的字節(jié)碼。設(shè)計者在第一版虛擬機規(guī)范中便承諾,時至今日,商業(yè)機構(gòu)和開源機構(gòu)已在之外發(fā)展出一大批可以在上運行的語言,如等。 本文已收錄【修煉內(nèi)功】躍遷之路 Java在誕生之初便提出 Write Once, Run Anywhere,各提供商發(fā)布很多不同平臺的虛擬機,這些虛擬機...
摘要:本文已收錄修煉內(nèi)功躍遷之路學(xué)習(xí)語言的時候,需要在不同的目標操作系統(tǒng)上或者使用交叉編譯環(huán)境,使用正確的指令集編譯成對應(yīng)操作系統(tǒng)可運行的執(zhí)行文件,才可以在相應(yīng)的系統(tǒng)上運行,如果使用操作系統(tǒng)差異性的庫或者接口,還需要針對不同的系統(tǒng)做不同的處理宏的 本文已收錄【修煉內(nèi)功】躍遷之路 showImg(https://segmentfault.com/img/bVbtpPd?w=2065&h=11...
閱讀 471·2023-04-25 17:26
閱讀 1494·2021-08-05 09:58
閱讀 1958·2019-08-30 13:17
閱讀 942·2019-08-28 17:52
閱讀 1060·2019-08-26 18:27
閱讀 1412·2019-08-26 14:05
閱讀 3607·2019-08-26 14:05
閱讀 1585·2019-08-26 10:45