第八章 虛擬機字節碼執行引擎html
一、運行時棧幀結構java
概述:git
局部變量表:github
package com.ecut.stack; /** * -verbose:gc */ public class SlotTest { public static void main(String[] args) { //placeholder的做用域被限制在花括號以內 { byte[] placeholder = new byte[64 * 1024 * 1024]; } //若是不增長這行,即沒有任何對局部變量表的讀寫操做,placeholder本來所佔用的Slot尚未被其餘變量所複用,因此做爲GC Roots一部分的局部變量表仍然保持着對它的關聯。 int a = 0 ; System.gc(); } }
運行結果:緩存
[GC (System.gc()) 68864K->66256K(125952K), 0.0020403 secs]
[Full GC (System.gc()) 66256K->664K(125952K), 0.0095304 secs]
操做數棧:安全
動態鏈接:數據結構
方法返回地址:架構
附加信息:jvm
二、方法調用ide
解析調用:
靜態分派:
package com.ecut.stack; public class StaticDispatch { static abstract class Human { } static class Man extends Human { } static class Woman extends Human { } public static void sayHello(Human guy) { System.out.println("hello guy"); } public static void sayHello(Man guy) { System.out.println("hello gentleman"); } public static void sayHello(Woman guy) { System.out.println("hello lady"); } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); sayHello(man); sayHello(woman); } }
運行結果:
hello guy
hello guy
「Human」稱爲變量的靜態類型,後面的「Man」稱爲變量的實際類型。虛擬機(準確地說是編譯器)在重載時是經過參數的靜態類型而不是實際類型做爲斷定依據的。所以,在編譯階段,Javac編譯器會根據參數的靜態類型決定使用哪一個重載版本。
全部依賴靜態類型來定位方法執行版本的分派動做稱爲靜態分派。靜態分派的典型應用是方法重載。靜態分派發生在編譯階段,所以肯定靜態分派的動做實際上不是由虛擬機來執行的。
編譯器雖然能肯定出方法的重載版本,但在不少狀況下這個重載版本並非「惟一的」,每每只能肯定一個「更加合適的」版本。
package com.ecut.stack; import java.io.Serializable; public class Overload { public static void sayHello(Object arg) { System.out.println("hello Object"); } public static void sayHello(int arg) { System.out.println("hello int"); } public static void sayHello(long arg) { System.out.println("hello long"); } public static void sayHello(Character arg) { System.out.println("hello Character"); } public static void sayHello(char arg) { System.out.println("hello char"); } public static void sayHello(char... arg) { System.out.println("hello char……"); } public static void sayHello(Serializable arg) { System.out.println("hello Serializable"); } public static void main(String[] args) { sayHello('a'); } }
運行結果:
hello char 這很好理解,'a'是一個char類型的數據,天然會尋找參數類型爲char的重載方法,若是註釋掉sayHello(char arg)方法,那輸出會變爲: hello int 這時發生了一次自動類型轉換,'a'除了能夠表明一個字符串,還能夠表明數字97(字符'a'的Unicode數值爲十進制數字97),所以參數類型爲int的重載也是合適的。咱們繼續註釋掉sayHello(int arg)方法,那輸出會變爲: hello long 這時發生了兩次自動類型轉換,'a'轉型爲整數97以後,進一步轉型爲長整數97L,匹配了參數類型爲long的重載。筆者在代碼中沒有寫其餘的類型如float、double等的重載,不過實際上自動轉型還能繼續發生屢次,按照char->int->long->float->double的順序轉型進行匹配。但不會匹配到byte和short類型的重載,由於char到byte或short的轉型是不安全的。咱們繼續註釋掉sayHello(long arg)方法,那輸出會變爲: hello Character 這時發生了一次自動裝箱,'a'被包裝爲它的封裝類型java.lang.Character,因此匹配到了參數類型爲Character的重載,繼續註釋掉sayHello(Character arg)方法,那輸出會變爲: hello Serializable 這個輸出可能會讓人感受摸不着頭腦,一個字符或數字與序列化有什麼關係?出現hello Serializable,是由於java.lang.Serializable是java.lang.Character類實現的一個接口,當自動裝箱以後發現仍是找不到裝箱類,可是找到了裝箱類實現了的接口類型,因此緊接着又發生一次自動轉型。char能夠轉型成int,可是Character是絕對不會轉型爲Integer的,它只能安全地轉型爲它實現的接口或父類。Character還實現了另一個接口java.lang.Comparable<Character>,若是同時出現兩個參數分別爲Serializable和Comparable<Character>的重載方法,那它們在此時的優先級是同樣的。編譯器沒法肯定要自動轉型爲哪一種類型,會提示類型模糊,拒絕編譯。程序必須在調用時顯式地指定字面量的靜態類型,如:sayHello((Comparable<Character>)'a'),才能編譯經過。下面繼續註釋掉sayHello(Serializable arg)方法,輸出會變爲: hello Object 這時是char裝箱後轉型爲父類了,若是有多個父類,那將在繼承關係中從下往上開始搜索,越接近上層的優先級越低。即便方法調用傳入的參數值爲null時,這個規則仍然適用。咱們把sayHello(Object arg)也註釋掉,輸出將會變爲: hello char……
解析與分派這二者之間的關係並非二選一的排他關係,它們是在不一樣層次上去篩選、肯定目標方法的過程。例如,前面說過,靜態方法會在類加載期就進行解析,而靜態方法顯然也是能夠擁有重載版本的,選擇重載版本的過程也是經過靜態分派完成的。
動態分派:
package com.ecut.stack; public class DynamicDispatch { static abstract class Human { protected abstract void sayHello(); } static class Man extends Human { @Override protected void sayHello() { System.out.println("man say hello"); } } static class Woman extends Human { @Override protected void sayHello() { System.out.println("woman say hello"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); man = new Woman(); man.sayHello(); } }
運行結果以下:
man say hello
woman say hello
woman say hello
使用javap -verbose DynamicDispatch .class命令
invokevirtual指令的運行時解析過程大體分爲如下幾個步驟:
因爲invokevirtual指令執行的第一步就是在運行期肯定接收者的實際類型,因此兩次調用中的invokevirtual指令把常量池中的類方法符號引用解析到了不一樣的直接引用上,這個過程就是Java語言中方法重寫的本質。咱們把這種在運行期根據實際類型肯定方法執行版本的分派過程稱爲動態分派。
單分派與多分派:
package com.ecut.stack; public class Dispatch { static class QQ { } static class _360 { } public static class Father { public void hardChoice(QQ arg) { System.out.println("father choose qq"); } public void hardChoice(_360 arg) { System.out.println("father choose 360"); } } public static class Son extends Father { public void hardChoice(QQ arg) { System.out.println("son choose qq"); } public void hardChoice(_360 arg) { System.out.println("son choose 360"); } } public static void main(String[] args) { Father father = new Father(); Father son = new Son(); father.hardChoice(new _360()); son.hardChoice(new QQ()); } }
運行結果以下:
father choose 360
son choose qq
咱們來看看編譯階段編譯器的選擇過程,也就是靜態分派的過程。這時選擇目標方法的依據有兩點:一是靜態類型是Father仍是Son,二是方法參數是QQ仍是360。此次選擇結果的最終產物是產生了兩條invokevirtual指令,兩條指令的參數分別爲常量池中指向Father.hardChoice(360)及Father.hardChoice(QQ)方法的符號引用。由於是根據兩個宗量進行選擇,因此Java語言的靜態分派屬於多分派類型。
再看看運行階段虛擬機的選擇,也就是動態分派的過程。在執行「son.hardChoice(new QQ())」這句代碼時,更準確地說,是在執行這句代碼所對應的invokevirtual指令時,因爲編譯期已經決定目標方法的簽名必須爲hardChoice(QQ),虛擬機此時不會關心傳遞過來的參數「QQ」究竟是「騰訊QQ」仍是「奇瑞QQ」,由於這時參數的靜態類型、實際類型都對方法的選擇不會構成任何影響,惟一能夠影響虛擬機選擇的因素只有此方法的接受者的實際類型是Father仍是Son。由於只有一個宗量做爲選擇依據,因此Java語言的動態分派屬於單分派類型。
虛擬機動態分派的實現:
動態類型語言支持:
JDK 1.7實現了JSR-292,新加入的java.lang.invoke包。這個包的主要目的是在以前單純依靠符號引用來肯定調用的目標方法這種方式之外,提供一種新的動態肯定目標方法的機制,稱爲MethodHandle。
package com.ecut.stack; import static java.lang.invoke.MethodHandles.lookup; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; public class MethodHandleTest{ static class ClassA{ public void println(String s){ System.out.println(s); } } public static void main(String[] args)throws Throwable{ Object obj=System.currentTimeMillis()%2==0?System.out:new ClassA(); /*不管obj最終是哪一個實現類,下面這句都能正確調用到println方法*/ getPrintlnMH(obj).invokeExact("MethodHandleTest"); } private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable{ /*MethodType:表明「方法類型」,包含了方法的返回值(methodType()的第一個參數)和具體參數(methodType()第二個及之後的參數)*/ MethodType mt=MethodType.methodType(void.class,String.class); /*lookup()方法來自於MethodHandles.lookup,這句的做用是在指定類中查找符合給定的方法名稱、方法類型,而且符合調用權限的方法句柄 由於這裏調用的是一個虛方法,按照Java語言的規則,方法第一個參數是隱式的,表明該方法的接收者,也便是this指向的對象, 這個參數之前是放在參數列表中進行傳遞的,而如今提供了bindTo()方法來完成這件事情*/ return lookup().findVirtual(reveiver.getClass(),"println",mt).bindTo(reveiver); } }
MethodHandle的基本用途,不管obj是何種類型(臨時定義的ClassA抑或是實現PrintStream接口的實現類System.out),均可以正確地調用到println()方法。
三、基於棧的字節碼解釋引擎
解釋執行的過程:
執行和編譯的兩種選擇:
源碼地址:
https://github.com/SaberZheng/jvm-test
推薦博客:
https://www.cnblogs.com/wade-luffy/archive/2016/11/13.html
轉載請於明顯處標明出處: