1. 程式人生 > 實用技巧 >5、JVM中的方法呼叫

5、JVM中的方法呼叫

1、虛擬機器執行時棧幀結構

棧幀在JVM中屬於執行緒私有的區域,用來儲存方法的區域性變量表、運算元棧、動態連線和完成出口,每一個方法從呼叫開始到執行完畢,都對應著一個棧幀在虛擬機器棧裡從入棧到出棧的過

程式編譯的時候,棧幀需要多大的區域性變量表、多深的運算元棧都已經完全確定了,並且寫入到方法表的code屬性中,因此一個棧幀分配的記憶體不會受到執行時變數的影響,只取決於虛擬機器的具體實現

2、虛擬機器執行時方法呼叫

方法呼叫並不是方法執行,方法呼叫的過程是為了來確定將要執行的方法,沒有涉及到方法內部邏輯的具體執行,程式編譯成Class檔案時檔案中不會包含連線的步驟,一切方法的呼叫在Class檔案中都是以符號引用來儲存的,而不是方法的直接引用,這時Java具有了強大的擴充套件性,需要在類載入的期間,甚至到執行期間才能確定方法的直接引用

因為class檔案中方法的呼叫都是以符號引用來儲存的,所以需要把符號引用替換成直接引用,這個替換的過程就是解析呼叫,class檔案進行解析的前提是編譯期可知,執行期不可變,方法在編譯時有一個可確定的呼叫版本,並且這個版本在執行時是不可變的

Java中符合這個前提的方法主要時靜態方法和私有方法,靜態方法與方法的型別直接關聯,私有方法在外部不可以被訪問,這兩種方法不可能通過繼承或者重寫等方式產生其他版本,它們都適合在類載入的階段進行解析,Java虛擬機器中提供了5條方法呼叫位元組碼指令:

invokestatic 呼叫靜態方法

invokespecial 呼叫例項構造器<init>方法、私有方法和super關鍵字

invokevirtual 呼叫非私有例項方法,比如 public 和 protected,大多數方法呼叫屬於這一種

invokeinterface 呼叫介面方法,會在執行時再確定實現這個介面的物件

invokedynamic 呼叫動態方法

invokestatic、invokespecial、invokevirtual、invokeinterface這四條指令的分派方法是固定在虛擬機器中的,invokedynamic指令的分派邏輯是由程式設計師設定的引導方法決定的

在解析呼叫階段中就可以確定有唯一的版本,在類載入的時候把符號引用解析為直接引用的方法叫做非虛方法,滿足非虛方法條件的方法有靜態方法、私有方法、例項構造器、父類方法4種,這些方法都能被invokestatic、invokespecial指令呼叫

分派呼叫,體現Java中的繼承、封裝、多型的特性,分派呼叫與解析呼叫不同的是,它不僅是靜態的也可以是動態的,根據分派依據可以分為單分派和多分派,進行組合一個有四類:靜態單分派、靜態多分派、動態單分派、動態多分派

 1 public class TNIO {
 2     static abstract class Father{};
 3     static class Boy extends Father{};
 4     static class Girl extends Father{};
 5     public void say(Boy f){
 6         System.out.println("this is a son");
 7     }
 8 
 9     public void say(Girl f){
10         System.out.println("this is a girl");
11     }
12 
13     public static void main(String[] args) {
14        //Father 就是變數的靜態型別 Boy就是變數的實際型別
15         Father f1 = new Boy();
16         Father f2 = new Girl();
17         TNIO tnio = new TNIO();
18         tnio.say(f1);
19         tnio.say(f2);
20     }
21 
22 }

依賴於靜態型別來定位方法執行版本的操作叫做靜態分派,常見於過載

執行期根據實際型別確定執行方法版本的操作叫做動態分派,常見於重寫

3、動態語言

invokedynamic 這個位元組碼是比較複雜。和反射類似,它用於一些動態的呼叫場景,但它和反射有著本質的不同,效率也比反射要高得多,這個指令通常在 Lambda 語法中出現

1 public class TNIO {
2     public static void main(String[] args) {
3         new Thread(()->{
4             System.out.println("this is a thread");
5         }).start();
6     }
7 
8 }

invokedynamic 指令的底層,是使用方法控制代碼(MethodHandle)來實現的。方法控制代碼是一個能夠被執行的引用,它可以指向靜態方法和例項方法,以及虛構的 get 和 set 方法

MethodHandle 就是方法控制代碼,通過這個控制代碼可以呼叫相應的方法,呼叫方法的流程為:
(1) 建立 MethodType,獲取指定方法的簽名(出參和入參)

(2) 在 Lookup 中查詢 MethodType 的方法控制代碼 MethodHandle

(3) 傳入方法引數通過 MethodHandle 呼叫方法

MethodType 表示一個方法型別的物件,每個 MethodHandle 都有一個 MethodType 例項,MethodType 用來指明方法的返回型別和引數型別,其有多個工廠方法的過載

MethodHandle.Lookup 可以通過相應的 findxxx 方法得到相應的 MethodHandle,相當於 MethodHandle 的工廠方法。查詢物件上的工廠方法對應於方法、建構函式和欄位的所有主要用例

findStatic 相當於得到的是一個 static 方法的控制代碼(類似於 invokestatic 的作用), findVirtual 找的是普通方法(類似於 invokevirtual 的作用)

其中需要注意的是 invokeinvokeExact,前者在呼叫的時候可以進行返回值和引數的型別轉換工作,而後者是精確匹配的,所以一般在使用是,往往 invoke 使用比 invokeExact 要多,因為 invokeExact 如果型別不匹配,則會拋錯

Lambda 表示式的捕獲與非捕獲

當 Lambda 表示式訪問一個定義在 Lambda 表示式體外的非靜態變數或者物件時,這個 Lambda 表示式稱為“捕獲的”,那麼“非捕獲”的 Lambda 表示式來就是 Lambda 表示式沒有訪問一個定義在 Lambda 表示式體外的非靜態變數或者物件

Lambda 表示式是否是捕獲的和效能悄然相關,一個非捕獲的 lambda 通常比捕獲的更高效,非捕獲的 lambda 只需要計算一次. 然後每次使用到它都會返回一個唯一的例項。而捕獲的 lambda 表示式每次使用時都需要重新計算一次,而且從目前實現來看,它很像例項化一個匿名內部類的例項

lambda 最差的情況效能內部類一樣, 好的情況肯定比內部類效能高