《深入理解Java虚拟机》读书笔记七
第八章 虛擬機字節(jié)碼執(zhí)行引擎
1、運行時棧幀結(jié)構(gòu)
概述:
- 棧幀是用于支持虛擬機進行方法調(diào)用的和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),他是虛擬機運行時數(shù)據(jù)區(qū)中的虛擬機棧的棧元素,棧幀存儲了方法的局部變量,操作數(shù)棧,動態(tài)連接和方法返回值等信息,每個方法從調(diào)用開始到執(zhí)行完成的過程都對應(yīng)著一個棧幀在虛擬機棧里面從入棧到出棧的過程。
- 一個線程中的方法調(diào)用鏈會很長,只有位于棧頂?shù)臈庞行?#xff0c;稱為當前棧幀,與這個棧幀相關(guān)聯(lián)的方法稱為當前方法。執(zhí)行引擎運行所有字節(jié)碼指令都只針對當前棧幀進行操作。
局部變量表:
- 局部變量表是一組變量存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。
- 在Java程序編譯為class文件時就在方法的code屬性的max_locals數(shù)據(jù)項中確定該方法所需要分配的局部變量表的最大容量。局部變量表的容量已變量槽為最小單位。
- 虛擬機通過索引定位的方式使用局部變量表,索引值的范圍從0開始至局部變量表最大Slot數(shù)量。
- 在方法執(zhí)行時,虛擬機是使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過程的,如果是實例方法(非static的方法),那么局部變量表中第0位索引的Slot默認是用于傳遞方法所屬對象實例的引用,在方法中可以通過關(guān)鍵字“this”來訪問這個隱含的參數(shù)。其余參數(shù)則按照參數(shù)表的順序來排列,占用從1開始的局部變量Slot,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的Slot。
- 局部變量表中的Slot是可重用的,方法體中定義的變量,其作用域并不一定會覆蓋整個方法體,如果當前字節(jié)碼PC計數(shù)器的值已經(jīng)超出了某個變量的作用域,那么這個變量對應(yīng)的Slot就可以交給其他變量使用。這樣的設(shè)計不僅僅是為了節(jié)省棧空間,在某些情況下Slot的復用會直接影響到系統(tǒng)的垃圾收集行為。 package com.ecut.stack;/*** -verbose:gc*/
public class SlotTest {public static void main(String[] args) {//placeholder的作用域被限制在花括號之內(nèi)
{byte[] placeholder = new byte[64 * 1024 * 1024];}//如果不增加這行,即沒有任何對局部變量表的讀寫操作,placeholder原本所占用的Slot還沒有被其他變量所復用,所以作為GC Roots一部分的局部變量表仍然保持著對它的關(guān)聯(lián)。int a = 0 ;System.gc();}
}
運行結(jié)果:
[GC (System.gc()) 68864K->66256K(125952K), 0.0020403 secs] [Full GC (System.gc()) 66256K->664K(125952K), 0.0095304 secs] - 局部變量定義了但是沒有初始化時不能使用的。
操作數(shù)棧:
- 也稱為操作棧,他是一個后入先出棧的棧,同局部變量一樣,操作數(shù)棧的最大深度也在編譯的時候?qū)懭氲搅薱ode屬性的max_stacks數(shù)據(jù)中,在方法執(zhí)行的任何時候,操作數(shù)棧的深度都不會超過在max_stacks數(shù)據(jù)項中設(shè)定的最大值。
- 當一個方法剛剛開始執(zhí)行的時候,這個方法的操作數(shù)棧是空的,在方法的執(zhí)行過程中,會有各種字節(jié)碼指令向操作數(shù)棧中寫入和提取內(nèi)容,也就是入棧出棧操作。
- Java虛擬機的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“棧”就是操作數(shù)棧。
動態(tài)連接:
- 每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接。
- 字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用為參數(shù),這些符號引用一部分會在類加載階段或第一次使用的時候轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。另外一部分將在每一次的運行期間轉(zhuǎn)化為直接引用,這部分稱為動態(tài)連接。
方法返回地址:
- 第一種退出方式是執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令,這時候可能會有返回值傳遞給上層的方法調(diào)用者(調(diào)用當前方法的方法稱為調(diào)用者),是否有返回值和返回值的類型將根據(jù)遇到何種方法返回指令來決定,這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)。
- 另外一種退出方式是,在方法執(zhí)行過程中遇到了異常,并且這個異常沒有在方法體內(nèi)得到處理,無論是Java虛擬機內(nèi)部產(chǎn)生的異常,還是代碼中使用athrow字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的上層調(diào)用者產(chǎn)生任何返回值的。
- 方法正常退出時,調(diào)用者的PC計數(shù)器的值就可以作為返回地址,棧幀中很可能會保存這個計數(shù)器值。而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會保存這部分信息。
- 方法退出的過程實際上等同于把當前棧幀出棧,因此退出時可能執(zhí)行的操作有:恢復上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整PC計數(shù)器的值以指向方法調(diào)用指令后面的一條指令等。
附加信息:
- 虛擬機規(guī)范允許具體的虛擬機實現(xiàn)增加一些規(guī)范里沒有描述的信息到棧幀之中,例如與調(diào)試相關(guān)的信息。
- 一般會把動態(tài)連接、方法返回地址與其他附加信息全部歸為一類,稱為棧幀信息。
2、方法調(diào)用
解析調(diào)用:
- 解析就是將方法的符號引用轉(zhuǎn)化成直接引用的,解析的前提是方法須在方法運行前就確定一個可調(diào)用的版本,并且這個版本在運行階段是不可改變的(編譯期可知,運行期不可變)。
- 只有用invokestatic和invokespecial指令調(diào)用的方法,都可以在解析階段確定調(diào)用版本,符合此條件的有靜態(tài)方法,私有方法,實例構(gòu)造器和父類方法四類。它們在類加載時即把符號引用解析為該方法的直接引用.這些方法可以稱為非虛方法。
- 解析調(diào)用是一個靜態(tài)過程,編譯期間就可以確定,分派調(diào)用可能是靜態(tài)的也可能是動態(tài)的,是實現(xiàn)多態(tài)性的體現(xiàn)。
靜態(tài)分派:
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);} }運行結(jié)果:
hello guy hello guy“Human”稱為變量的靜態(tài)類型,后面的“Man”稱為變量的實際類型。虛擬機(準確地說是編譯器)在重載時是通過參數(shù)的靜態(tài)類型而不是實際類型作為判定依據(jù)的。因此,在編譯階段,Javac編譯器會根據(jù)參數(shù)的靜態(tài)類型決定使用哪個重載版本。
所有依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動作稱為靜態(tài)分派。靜態(tài)分派的典型應(yīng)用是方法重載。靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分派的動作實際上不是由虛擬機來執(zhí)行的。
編譯器雖然能確定出方法的重載版本,但在很多情況下這個重載版本并不是“唯一的”,往往只能確定一個“更加合適的”版本。
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');} }運行結(jié)果:
hello char這很好理解,'a'是一個char類型的數(shù)據(jù),自然會尋找參數(shù)類型為char的重載方法,如果注釋掉sayHello(char arg)方法,那輸出會變?yōu)?#xff1a;hello int這時發(fā)生了一次自動類型轉(zhuǎn)換,'a'除了可以代表一個字符串,還可以代表數(shù)字97(字符'a'的Unicode數(shù)值為十進制數(shù)字97),因此參數(shù)類型為int的重載也是合適的。我們繼續(xù)注釋掉sayHello(int arg)方法,那輸出會變?yōu)?#xff1a;hello long這時發(fā)生了兩次自動類型轉(zhuǎn)換,'a'轉(zhuǎn)型為整數(shù)97之后,進一步轉(zhuǎn)型為長整數(shù)97L,匹配了參數(shù)類型為long的重載。筆者在代碼中沒有寫其他的類型如float、double等的重載,不過實際上自動轉(zhuǎn)型還能繼續(xù)發(fā)生多次,按照char->int->long->float->double的順序轉(zhuǎn)型進行匹配。但不會匹配到byte和short類型的重載,因為char到byte或short的轉(zhuǎn)型是不安全的。我們繼續(xù)注釋掉sayHello(long arg)方法,那輸出會變?yōu)?#xff1a;hello Character這時發(fā)生了一次自動裝箱,'a'被包裝為它的封裝類型java.lang.Character,所以匹配到了參數(shù)類型為Character的重載,繼續(xù)注釋掉sayHello(Character arg)方法,那輸出會變?yōu)?#xff1a;hello Serializable這個輸出可能會讓人感覺摸不著頭腦,一個字符或數(shù)字與序列化有什么關(guān)系?出現(xiàn)hello Serializable,是因為java.lang.Serializable是java.lang.Character類實現(xiàn)的一個接口,當自動裝箱之后發(fā)現(xiàn)還是找不到裝箱類,但是找到了裝箱類實現(xiàn)了的接口類型,所以緊接著又發(fā)生一次自動轉(zhuǎn)型。char可以轉(zhuǎn)型成int,但是Character是絕對不會轉(zhuǎn)型為Integer的,它只能安全地轉(zhuǎn)型為它實現(xiàn)的接口或父類。Character還實現(xiàn)了另外一個接口java.lang.Comparable<Character>,如果同時出現(xiàn)兩個參數(shù)分別為Serializable和Comparable<Character>的重載方法,那它們在此時的優(yōu)先級是一樣的。編譯器無法確定要自動轉(zhuǎn)型為哪種類型,會提示類型模糊,拒絕編譯。程序必須在調(diào)用時顯式地指定字面量的靜態(tài)類型,如:sayHello((Comparable<Character>)'a'),才能編譯通過。下面繼續(xù)注釋掉sayHello(Serializable arg)方法,輸出會變?yōu)?#xff1a;hello Object這時是char裝箱后轉(zhuǎn)型為父類了,如果有多個父類,那將在繼承關(guān)系中從下往上開始搜索,越接近上層的優(yōu)先級越低。即使方法調(diào)用傳入的參數(shù)值為null時,這個規(guī)則仍然適用。我們把sayHello(Object arg)也注釋掉,輸出將會變?yōu)?#xff1a;hello char……解析與分派這兩者之間的關(guān)系并不是二選一的排他關(guān)系,它們是在不同層次上去篩選、確定目標方法的過程。例如,前面說過,靜態(tài)方法會在類加載期就進行解析,而靜態(tài)方法顯然也是可以擁有重載版本的,選擇重載版本的過程也是通過靜態(tài)分派完成的。
動態(tài)分派:
package com.ecut.stack;public class DynamicDispatch {static abstract class Human {protected abstract void sayHello();}static class Man extends Human {@Overrideprotected void sayHello() {System.out.println("man say hello");}}static class Woman extends Human {@Overrideprotected 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();} }運行結(jié)果如下:
man say hello woman say hello woman say hello使用javap -verbose DynamicDispatch .class命令
invokevirtual指令的運行時解析過程大致分為以下幾個步驟:
- 找到操作數(shù)棧頂?shù)牡谝粋€元素所指向的對象的實際類型,記作C。
- 如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權(quán)限校驗,如果通過則返回這個方法的直接引用,查找過程結(jié)束;如果不通過,則返回java.lang.IllegalAccessError異常。
- 否則,按照繼承關(guān)系從下往上依次對C的各個父類進行第2步的搜索和驗證過程。
- 如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。
由于invokevirtual指令執(zhí)行的第一步就是在運行期確定接收者的實際類型,所以兩次調(diào)用中的invokevirtual指令把常量池中的類方法符號引用解析到了不同的直接引用上,這個過程就是Java語言中方法重寫的本質(zhì)。我們把這種在運行期根據(jù)實際類型確定方法執(zhí)行版本的分派過程稱為動態(tài)分派。
單分派與多分派:
- 方法的接收者與方法的參數(shù)統(tǒng)稱為方法的宗量
- 根據(jù)分派基于多少種宗量,可以將分派劃分為單分派和多分派兩種。單分派是根據(jù)一個宗量對目標方法進行選擇,多分派則是根據(jù)多于一個宗量對目標方法進行選擇。 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());}
}
運行結(jié)果如下:
father choose 360 son choose qq我們來看看編譯階段編譯器的選擇過程,也就是靜態(tài)分派的過程。這時選擇目標方法的依據(jù)有兩點:一是靜態(tài)類型是Father還是Son,二是方法參數(shù)是QQ還是360。這次選擇結(jié)果的最終產(chǎn)物是產(chǎn)生了兩條invokevirtual指令,兩條指令的參數(shù)分別為常量池中指向Father.hardChoice(360)及Father.hardChoice(QQ)方法的符號引用。因為是根據(jù)兩個宗量進行選擇,所以Java語言的靜態(tài)分派屬于多分派類型。
再看看運行階段虛擬機的選擇,也就是動態(tài)分派的過程。在執(zhí)行“son.hardChoice(new QQ())”這句代碼時,更準確地說,是在執(zhí)行這句代碼所對應(yīng)的invokevirtual指令時,由于編譯期已經(jīng)決定目標方法的簽名必須為hardChoice(QQ),虛擬機此時不會關(guān)心傳遞過來的參數(shù)“QQ”到底是“騰訊QQ”還是“奇瑞QQ”,因為這時參數(shù)的靜態(tài)類型、實際類型都對方法的選擇不會構(gòu)成任何影響,唯一可以影響虛擬機選擇的因素只有此方法的接受者的實際類型是Father還是Son。因為只有一個宗量作為選擇依據(jù),所以Java語言的動態(tài)分派屬于單分派類型。
虛擬機動態(tài)分派的實現(xiàn):
- 由于動態(tài)分派是非常頻繁的動作,而且動態(tài)分派的方法版本選擇過程需要運行時在類的方法元數(shù)據(jù)中搜索合適的目標方法,因此在虛擬機的實際實現(xiàn)中基于性能的考慮,大部分實現(xiàn)都不會真正地進行如此頻繁的搜索。面對這種情況,最常用的“穩(wěn)定優(yōu)化”手段就是為類在方法區(qū)中建立一個虛方法表(Vritual Method Table,也稱為vtable,與此對應(yīng)的,在invokeinterface執(zhí)行時也會用到接口方法表——Inteface Method Table,簡稱itable),使用虛方法表索引來代替元數(shù)據(jù)查找以提高性能。
- 虛方法表中存放著各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那子類的虛方法表里面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實現(xiàn)入口。如果子類中重寫了這個方法,子類方法表中的地址將會替換為指向子類實現(xiàn)版本的入口地址。Son重寫了來自Father的全部方法,因此Son的方法表沒有指向Father類型數(shù)據(jù)的箭頭。但是Son和Father都沒有重寫來自O(shè)bject的方法,所以它們的方法表中所有從Object繼承來的方法都指向了Object的數(shù)據(jù)類型。
- 方法表一般在類加載的連接階段進行初始化,準備了類的變量初始值后,虛擬機會把該類的方法表也初始化完畢。
動態(tài)類型語言支持:
- 動態(tài)類型語言的關(guān)鍵特征是它的類型檢查的主體過程是在運行期而不是編譯期,滿足這個特征的語言有很多,常用的包括:APL、Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk和Tcl等。
- 相對的,在編譯期就進行類型檢查過程的語言(如C++和Java等)就是最常用的靜態(tài)類型語言。
-
JDK 1.7實現(xiàn)了JSR-292,新加入的java.lang.invoke包。這個包的主要目的是在之前單純依靠符號引用來確定調(diào)用的目標方法這種方式以外,提供一種新的動態(tài)確定目標方法的機制,稱為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最終是哪個實現(xiàn)類,下面這句都能正確調(diào)用到println方法*/getPrintlnMH(obj).invokeExact("MethodHandleTest");}private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable{/*MethodType:代表“方法類型”,包含了方法的返回值(methodType()的第一個參數(shù))和具體參數(shù)(methodType()第二個及以后的參數(shù))*/MethodType mt=MethodType.methodType(void.class,String.class);/*lookup()方法來自于MethodHandles.lookup,這句的作用是在指定類中查找符合給定的方法名稱、方法類型,并且符合調(diào)用權(quán)限的方法句柄因為這里調(diào)用的是一個虛方法,按照Java語言的規(guī)則,方法第一個參數(shù)是隱式的,代表該方法的接收者,也即是this指向的對象,這個參數(shù)以前是放在參數(shù)列表中進行傳遞的,而現(xiàn)在提供了bindTo()方法來完成這件事情*/return lookup().findVirtual(reveiver.getClass(),"println",mt).bindTo(reveiver);} }MethodHandle的基本用途,無論obj是何種類型(臨時定義的ClassA抑或是實現(xiàn)PrintStream接口的實現(xiàn)類System.out),都可以正確地調(diào)用到println()方法。
- MethodHandle與Reflection的區(qū)別
- 從本質(zhì)上講,Reflection和MethodHandle機制都是在模擬方法調(diào)用,但Reflection是在模擬Java代碼層次的方法調(diào)用,而MethodHandle是在模擬字節(jié)碼層次的方法調(diào)用。在MethodHandles.lookup中的3個方法——findStatic()、findVirtual()、findSpecial()正是為了對應(yīng)于invokestatic、invokevirtual&invokeinterface和invokespecial這幾條字節(jié)碼指令的執(zhí)行權(quán)限校驗行為,而這些底層細節(jié)在使用Reflection API時是不需要關(guān)心的。
- Reflection中的java.lang.reflect.Method對象遠比MethodHandle機制中的java.lang.invoke.MethodHandle對象所包含的信息多。前者是方法在Java一端的全面映像,包含了方法的簽名、描述符以及方法屬性表中各種屬性的Java端表示方式,還包含執(zhí)行權(quán)限等的運行期信息。而后者僅僅包含與執(zhí)行該方法相關(guān)的信息。用通俗的話來講,Reflection是重量級,而MethodHandle是輕量級。
- 由于MethodHandle是對字節(jié)碼的方法指令調(diào)用的模擬,所以理論上虛擬機在這方面做的各種優(yōu)化(如方法內(nèi)聯(lián)),在MethodHandle上也應(yīng)當可以采用類似思路去支持(但目前實現(xiàn)還不完善)。而通過反射去調(diào)用方法則不行。
- MethodHandle與Reflection除了上面列舉的區(qū)別外,最關(guān)鍵的一點還在于去掉前面討論施加的前提“僅站在Java語言的角度來看”:Reflection API的設(shè)計目標是只為Java語言服務(wù)的,而MethodHandle則設(shè)計成可服務(wù)于所有Java虛擬機之上的語言,其中也包括Java語言。
- nvokedynamic指令與MethodHandle機制的作用是一樣的,都是為了解決原有4條“invoke*”指令方法分派規(guī)則固化在虛擬機之中的問題,把如何查找目標方法的決定權(quán)從虛擬機轉(zhuǎn)嫁到具體用戶代碼之中,讓用戶(包含其他語言的設(shè)計者)有更高的自由度。
3、基于棧的字節(jié)碼解釋引擎
解釋執(zhí)行的過程:
執(zhí)行和編譯的兩種選擇:
- 基于棧的指令集與基于寄存器的指令集
- 基于棧的指令集主要的優(yōu)點就是可移植
- 棧架構(gòu)指令集的主要缺點是執(zhí)行速度相對來說會稍慢一些,因為出棧、入棧操作本身就產(chǎn)生了相當多的指令數(shù)量。更重要的是,棧實現(xiàn)在內(nèi)存之中,頻繁的棧訪問也就意味著頻繁的內(nèi)存訪問,相對于處理器來說,內(nèi)存始終是執(zhí)行速度的瓶頸。盡管虛擬機可以采取棧頂緩存的手段,把最常用的操作映射到寄存器中避免直接內(nèi)存訪問,但這也只能是優(yōu)化措施而不是解決本質(zhì)問題的方法。由于指令數(shù)量和內(nèi)存訪問的原因,所以導致了棧架構(gòu)指令集的執(zhí)行速度會相對較慢。
源碼地址:
https://github.com/SaberZheng/jvm-test
推薦博客:
https://www.cnblogs.com/wade-luffy/archive/2016/11/13.html
轉(zhuǎn)載請于明顯處標明出處:
https://www.cnblogs.com/AmyZheng/p/10548753.html
轉(zhuǎn)載于:https://www.cnblogs.com/AmyZheng/p/10548753.html
總結(jié)
以上是生活随笔為你收集整理的《深入理解Java虚拟机》读书笔记七的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Win7环境配置Oracle 11g安装
- 下一篇: 第5章 一等函数