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