Java虛擬機執行引擎知識總結
執行引擎
也只有幾個概念, JVM方法調用和執行的基礎數據結構是 棧幀, 是內存區域中 虛擬機棧中的棧元素, 每一個方法的執行就對應著一個棧幀在虛擬機棧中出棧入棧的過程.
棧幀:則是包含有局部變量表, 操作數棧, 動態連接, 方法返回地址, 附加信息.
1 局部變量表:
存儲單位是 slot, 一個slot占據32位, 對于64位的數據類型, 則是分配連續兩個slot空間. 而對于一個非靜態方法而言, 有一個隱藏參數, 為 this, 而在局部變量表中的變量存儲順序則是
this -> 方法參數 -> 方法體內的變量(slot可以重用, 超出作用域即可復用.) 方法在編譯完成后, 其所需的空間已經確定.
(這里也是需要注意的一個地方, 變量的作用域常常會覆蓋整個方法, 即使變量已經不再使用, 但只要還在作用域內, 其slot空間就無法給其他變量使用, 因此, 最好是在需要使用到變量時, 定義在合理的作用域范圍內.)
2 操作數棧:
在操作數棧中需要注意,其數據類型必須與字節碼指令的序列嚴格匹配.
3 動態連接: 稍后詳解
4 方法返回地址:
方法有兩種退出方式, 正常退出, 異常退出, 當正常退出后, 會恢復上層方法的局部變量表, 操作數棧, 并把方法返回結果壓入調用者的操作數棧.
方法調用
方法調用階段的唯一目的是, 確定調用方法的版本究竟是哪一個.
在Java虛擬機中提供了5條方法調用的相關指令:
invokestatic: 調用靜態方法
invokespecial: 調用實例構造器方法, 私有方法, 父類方法
invokevirtual: 調用所有的虛方法
invokeinterface: 調用所有的接口方法
invokedynamic: 先在運行時動態解析出調用點限定符所引用的方法, 然后再執行該方法.
虛方法是非虛方法的補集, 什么是非虛方法呢? 能夠在編譯器就確定將要調用的究竟是哪個方法, 進而將該方法的符號引用 轉換為 相應的直接引用的 方法就被稱作非虛方法.
我們知道在類加載時, 在相應的類信息中, 存有對應方法的相關信息, 常量池中存有相關直接引用. 在類加載的解析階段, 即會將這部分的符號引用轉換為直接引用.
那么什么方法才滿足這種條件呢?
能夠被invokespecial 和 invokestatic指令調用的方法, 都是可以在編譯器確定的方法, 即靜態方法, 私有方法, 父類方法(super.), 實例構造器.
在final方法是個特殊點, 雖然final方法的執行為 invokevirtual, 但它依然屬于非虛方法, 不難理解, final方法不能夠被重寫.
方法分派(dispatch)
1 靜態分派
對于代碼
Human man = new Man();
其中Human被稱為變量的靜態類型, 也叫外觀類型, 而 Man則是變量的實際類型. 而一個變量的靜態類型, 在聲明時即已經確定, 僅僅在使用時才能夠臨時轉換靜態類型, 但變量本身的靜態類型并不會改變, 實際類型的變化只有在運行期才能確定.
//實際類型變化 Human man = new Man(); man = new Woman(); //靜態類型的變化 method((Man) man); method((Woman) man);
而當我們在重載方法時, 向方法中傳入的參數類型, 即是靜態類型.因此 重載是一種 可以在編譯期就被確定執行方法版本 的行為.
2 動態分派
動態分派 與 重寫息息相關.
static class Human{ void sayHello() { System.out.println('human say hello'); } } static class Man extends Human{ @Override void sayHello() { System.out.println('man say hello'); } } void sayHello(Human man) { man.sayHello(); } public static void main(String[] args) { Human man = new Man(); Human human = new Human(); new Main().sayHello(man); new Main().sayHello(human); } //out: man say hello human say hello
結果不必多做解釋, 而現在的問題在于, 虛擬機如何知道, 究竟調用的是哪個方法?
0: new #3 // class Main$Man 3: dup 4: invokespecial #4 // Method Main$Man.'<init>':()V 7: astore_1 8: new #5 // class Main$Human 11: dup 12: invokespecial #6 // Method Main$Human.'<init>':()V 15: astore_2 16: new #7 // class Main 19: dup 20: invokespecial #8 // Method '<init>':()V 23: aload_1 24: invokevirtual #9 // Method sayHello:(LMain$Human;)V 27: new #7 // class Main 30: dup 31: invokespecial #8 // Method '<init>':()V 34: aload_2 35: invokevirtual #9 // Method sayHello:(LMain$Human;)V 38: return
其中主要關注幾個方法的執行點, invokespecial不用多說, 之前提到過, 是執行 構造器方法時 的指令
而 invokevirtual 則正是執行 main.sayHello(), 方法的指令, 指令的運行時解析過程大致如下:
而其中的關鍵點就在于, 取到的是 對象的實際類型.
1 找到操作數棧頂的第一個元素的所指對象的實際類型, 記做C
2 如果在C中找到與描述符 和 簡單名稱都相符的方法, 進行訪問校驗, 如果可以則返回方法的直接引用, 否則拋出 IllegalAccessError異常
3 否則按照繼承關系 從下向上對C的各個父類進行第二步的搜索驗證過程.
4 如果始終找不到, 拋出異常.
動態類型語言
這也是要提到的關于 invokedynamic指令的主要目的。
動態類型語言的概念是: 意思就是類型的檢查是在運行時做的而非編譯期。
而Java本身則是靜態類型語言, 這一點又在哪里能夠體現呢?
obj.println('language');
如果處在java環境中,且obj的靜態語言類型是 java.io.PrintStream, 那么obj本身的實際類型也必須是PrintStream的子類才行, 哪怕本身存在 println方法也不可以, 但同樣的問題放在 javascript中就不同了, 只要實際類型中存在println方法, 執行就不會有任何問題.
這點就是因為, java在編譯時已經將其完整的符號引用生成出來, 如果注意到的話, 會發現無論是動態分派還是靜態分派, 在編譯的指令中都是已經精確到相應類的某一個方法中了, 如此, 自然只能夠在有限的范圍內略做調整, 如果超出了當前類的范圍, 就無法調用了.
jvm虛擬機并不僅僅是java語言的虛擬機, 那么如何為動態類型語言提供支持就是一個問題了, 并且在目前java8中的lamda表達式中也應用的是 invokedynamic指令.
MethodHandle
而與之相關的jar包則是 java.lang.invoke, 相關的類則是 MethodHandle.
在這里我也并不想再談 MethodHandle的使用方法, 網上資料實在不少.
需要提到的是, 它的功能和java的反射略有相似, 通過方法名, class, 就可以調用相應的方法. 但它比起反射要輕量級; 且Reflection是在模擬Java代碼的調用, MethodHandle是在模仿字節碼層面的調用.
這個方法不失為是在動態調用中除了反射之外的另一種選擇.
基于棧解釋器的執行過程
其實本文更像是在 前一篇博客中 java內存區域中的虛擬機棧的一種補充說明.
而真實的執行流程, 我想通過下文的代碼來看:
public int add() { int a = 100; int b = 200; int c = 300; return (a + b) * c;}-- javap -verbose Mainpublic int add();// 返回類型為 intdescriptor: ()Iflags: ACC_PUBLICCode://需要深度為2的操作數棧, 4個slot的局部變量空間, 有一個參數為 this stack=2, locals=4, args_size=1 //將100推入操作數棧頂, 棧:100 0: bipush 100 //將棧頂的數據出棧并存儲到局部變量表的第一個slot中(從0開始) //此時:棧: - 局部變量表: slot1 100 2: istore_1 //與上面類似,重復過程 3: sipush 200 6: istore_2 7: sipush 300 //此時:棧: - 局部變量表: slot1 100 slot2 200 slot3 300 10: istore_3 //將局部變量表 slot1的值復制到 棧頂 11: iload_1 //將局部變量表 slot2的值復制到 棧頂 此時:棧: 200 100 12: iload_2 //棧頂兩個元素出棧, 并相加, 結果重新入棧. 此時: 棧: 300 13: iadd //將局部變量表 slot3的值復制到 棧頂 此時:棧: 300 300 14: iload_3 //將棧頂元素相乘, 結果重新入棧 15: imul //將棧頂的結果返回給方法調用者. 方法執行結束 16: ireturn LineNumberTable: line 85: 0 line 86: 3 line 87: 7 line 88: 11
基于棧的執行引擎正是通過這樣出棧入棧的方式完成指令, 而基于寄存器的則不然, 是將操作數存入寄存器, 同時將輸入值也就是指令參數 與 某寄存器的存儲值相加. 區別就在于存儲位置, 以及參數問題, 基于棧的大部分指令都是無參數指令, 指令很明確的規定了 要用哪幾個棧元素, 棧元素的類型是什么.
我們平常所使用的電腦, 其 X86指令集, 正是基于寄存器的指令集.
優缺點則是: 基于棧, 可移植性較強, 但速度比較慢, 慢的原因一是需要許多冗余操作, 代碼. 二是基于棧是基于內存的操作方式, 而內存的速度比起寄存器更是要慢上許多.
總結
本文大致介紹這樣幾點:
java多態在 jvm層次的實現.
為什么說jvm執行引擎是基于棧的執行引擎, 以及究竟是怎樣一個流程.
以上就是Java虛擬機執行引擎知識總結的詳細內容,更多關于Java虛擬機執行引擎的資料請關注好吧啦網其它相關文章!
相關文章:
