##1. 運行時棧幀結構 棧幀
是虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧(VM Stack)的棧元素。每個棧幀都包括了:局部變量表
、操做數棧
、動態鏈接
、方法返回地址和一些額外的附加信息
。每個方法從調用開始到執行結束的過程,都對應着一個棧幀在虛擬機棧裏從入棧到出棧
的過程。 在編譯程序代碼的時候,棧幀中須要多大的局部變量表、多深的操做數棧都已經徹底肯定,並寫入到方法表的Code屬性之中。所以一個棧幀須要分配多大的內存不會受到運行期變量數據的影響。java
###1.1 局部變量表 局部變量表(Local Variable Table)是一組變量值
的存儲空間,用於存放方法參數
和方法中的局部變量的值
。 局部變量中的容量以容量槽(Variable Slot)爲最小單位。虛擬機經過索引定位
的方式使用局部變量表,索引值的範圍從0開始至局部變量表最大的Slot容量
。 在方法執行時,虛擬機是經過局部變量表完成參數值到參數變量列表的傳遞過程的,若是執行的實例方法,那局部變量表中第0位索引的Slot默認用於傳遞方法所屬對象實例的引用,而後按順序存儲存儲方法的參數,最後根據方法體內定義的變量順序和做用域分配其他的Slot。數據結構
###1.2 操做數棧 操做數棧(Oprand Stack)是一個先進先出的棧。跟局部變量表同樣,操做數棧的深度在編譯的時候已寫入到Class文件中Code屬性的max_stacks數據項中。通常32位數據類型佔用一個Slot容量,64位數據類型佔用兩個Slot容量。ide
###1.3 動態鏈接 每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接。性能
###1.4 方法返回地址 當一個方法開始執行後,只有兩種方式能夠退出這個方法。第一種:執行引擎遇到方法的返回指令,這種退出方法的方式稱爲正常完成出口
;第二種:方法在執行過程當中遇到異常,而且這個方法沒有在方法體內獲得處理,不管是Java虛擬機內部產生的異常,仍是代碼中經過athrow字節碼指令產生的異常, 只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會致使方法退出,這種方式稱爲異常完成出口
。不管採用何種方式退出,在退出方法以後,都要返回到方法的調用位置。通常來講,方法正常退出時,調用者的PC計數器的值能夠做爲返回地址,棧幀中極可能會保存這個計數器的值。而方法異常退出時,返回地址是經過異常異常處理器表來肯定的。code
##2. 方法調用 方法調用並不等同於方法執行,方法調用階段惟一的任務
就是肯定被調用方法的版本(即調用哪一個方法),暫時不涉及方法內部的具體運行過程。對象
###2.1 解析 全部方法調用中的目標方法在Class文件裏都是一個常量池的符號引用,在類加載的解析階段
,會將其中一部分符號引用轉化爲直接引用,這種轉化成立的前提是:方法在程序真正運行以前就有一個肯定的調用版本,而且這個方法的調用版本在運行期是不可變的
。這類方法的調用成爲解析(Resolution)
。 在Java語言中符合編譯器可知,運行期不可變
這個要求的方法,主要包括:靜態方法
和私有方法
。這兩個方法都適合在類加載階段進行解析。 在Java虛擬機中提供了5中方法調用字節碼指令:繼承
<init>
方法、私有方法和父類方法只要能被invokestatic和invokespecial指令調用的方法,均可以在解析階段中肯定肯定惟一的調用版本,符合這個條件的方法有:靜態方法
、私有方法
、實例構造器
、弗雷方法
4類,它們在類加載的時候就會把符號引用解析爲該方法的直接引用。這些方法能夠稱爲非虛方法
,與之相反,其餘方法稱爲虛方法
。 雖然被final修飾的方法是使用invokevirtual指令來調用的,可是因爲它沒法被覆蓋,沒有其餘版本,因此final方法是虛方法
。索引
###2.1 分派接口
####2.1.1 靜態分派內存
Human man = new Man();
上面代碼中的「Human」稱爲變量的靜態類型
,或者成爲外觀類型
,後面的Man
則稱爲變量的實際類型。靜態類型的變化僅僅在使用時發生,而變量自己的靜態類型不會被改變,最終的靜態類型是在編譯器可知的,而實際類型變化的結果在運行期纔可肯定。虛擬機編譯器(準確來講是)在重載時是經過靜態類型而不是實際類型做爲判斷依據的
。 全部依賴靜態類型來定位方法執行版本的分派動做成爲靜態分派
。靜態分派的典型應用是方法重載。
###2.1.2 動態分派 在運行期根據根據實際類型來肯定方法執行版本的分派過程成爲動態分派
。動態分派的主要體現是:重寫(Override)。invokevirtual指令的運行時解析過程分爲一下步驟:
###2.1.3 單分派與多分派 方法的接受者與方法的參數統稱爲方法的宗量
。單分派是根據一個宗量對目標方法進行選擇。多分派是根據多於一個宗量對目標方法進行選擇。 編譯階段編譯器的選擇過程(即靜態分派過程),Java語言的靜態分派屬於多分派。
###2.1.4 虛擬機動態分派的實現 動態分派的方法選擇過程須要在運行時類的方法元數據中搜索合適的目標方法,基於性能的考慮,一般在類方法區中創建一個虛方法表(Virtual Method Table,與此對應,在invokeinterface執行時也會用到接口方法表-Interface Method Table),使用虛方法表索引來代替元數據查找以提升性能。 虛方法表中存放着方法的實際入口地址,若是某個方法在子類沒有被重寫,那子類的虛方法表的地址入口與父類相同方法的地址入口是一致的,都指向父類的實際入口;若是子類重寫了這個方法,那子類虛方法表中的地址將會替換爲指向子類實現版本的入口地址。 虛方法表通常在類加載的鏈接階段進行初始化,準備了類的變量初始值後,虛擬機會把該類的虛方法表也初始化完畢。
###2.2 動態類型語言支持
####2.2.1 動態類型語言 動態類型語言的關鍵特性是:它的類型檢查的主題過程是在運行期而不是編譯期
;在編譯期就進行類型檢查過程的語言成爲靜態類型語言
。變量無類型而變量值纔有類型
也是動態類型語言的一個重要特徵。
####2.2.2 java.lang.invoke包 這個包的主要目的:在以前單純依靠符號引用來肯定調用的目標方法這種方式以外,提供了一種新的動態肯定目標方法的機制,稱爲MethodHandle
。 MethodHandle演示:
import static java.lang.invoke.MethodHandles.Lookup; import java.lang.invoke.MethodType; import java.lang.invoke.MethodHandle; 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(); //下面這句能夠正確調用到println方法 getPrintlnMH(obj).invokeExact("icyfenix"); } private static MethodHandle getPrintlnMH(Object receiver) throws Throwable { //MethodType: 表明「方法類型」,包含了方法的返回值(methodType()的第一個參數)和具體參數(methodType()第二個及之後的參數) MethodType mt = MethodType.methodType(void.class,String.class); //lookup()來自於MethodHandle.Lookup, //這句的意思是:在指定類中查找符合給定的方法名稱、方法類型,而且符合調用權限的方法句柄。 //由於這裏調用的是一個虛方法,方法的第一個參數應該是隱式的,表明方法的接收者, //此參數之前放在參數列表中進行傳遞,如今提供了bindTo()方法來完成這件事。 return lookup().findVirtual(receiver.getClass(),"println",mt).bindTo(receiver); } }