字節碼是什麼東西?
以下是百度的解釋:
字節碼(Byte-code)是一種包含執行程序、由一序列 op 代碼/數據對組成的二進制文件。字節碼是一種中間碼,它比機器碼更抽象。
它經常被看作是包含一個執行程序的二進制文件,更像一個對象模型。字節碼被這樣叫是因為通常每個 opcode 是一字節長,
但是指令碼的長度是變化的。每個指令有從 0 到 255(或十六進制的: 00 到FF)的一字節操作碼,被參數例如寄存器或內存地址跟隨。
說了這麼多,你可能還是不明白到底是什麼東西。好吧,簡單點,就是java編譯以後的那個東東,“.class”文件。
所以class文件就是字節碼文件,是由虛擬機執行的文件。也就是java語言和C & C++語言的區別就是,整個編譯執行過程多了一個虛擬
機這一步。這個在“深入理解java虛擬機(3)---類的結構” 一文中已經解釋,這是一個裡程碑式的設計。上一節講了虛擬機是如何加載
一個class的,這一節就講解虛擬機是如何執行class文件的。
java虛擬機規范,規定了虛擬機字節碼的執行概念模型。具體的虛擬機可以有不同的實現。
棧是每個線程獨有的內存。
棧幀存儲了局部變量表,操作數棧,動態連接,和返回地址等。
每一個方法的執行 對應的一個棧幀在虛擬機裡面從如棧到出棧的過程。
只有位於棧頂的棧幀才有有效的,對應的方法稱為當前方法。
執行引擎運行的所有指令只針對當前棧幀和當前方法。
局部變量表存放的一組變量的存儲空間。存放方法參數和方法內部定義的局部變量表。
在java編譯成class的時候,已經確定了局部變量表所需分配的最大容量。
局部變量表的最小單位是一個Slot。
虛擬機規范沒有明確規定一個Slot占多少大小。只是規定,它可以放下boolean,byte,...reference &return address.
reference 是指一個對象實例的引用。關於reference的大小,目前沒有明確的指定大小。但是我們可以理解為它就是類似C++中的指針。
局部變量表的讀取方式是索引,從0開始。所以局部變量表可以簡單理解為就是一個表.
局部變量表的分配順序如下:
this 引用。可以認為是隱式參數。
方法的參數表。
根據局部變量順序,分配Solt。
一個變量一個solt,64為的占2個solt。java中明確64位的是long & double
為了盡可能的節約局部變量表,Solt可以重用。
注意:局部變量只給予分配的內存,沒有class對象的准備階段,所以局部變量在使用前,必須先賦值。
操作數棧在概念上很像寄存器。
java虛擬機無法使用寄存器,所以就有操作數棧來存放數據。
虛擬機把操作數棧作為它的工作區——大多數指令都要從這裡彈出數據,執行運算,然後把結果壓回操作數棧。
比如,iadd指令就要從操作數棧中彈出兩個整數,執行加法運算,其結果又壓回到操作數棧中,看看下面的示例,
它演示了虛擬機是如何把兩個int類型的局部變量相加,再把結果保存到第三個局部變量的:
begin
iload_0 // push the int in local variable 0 onto the stack
iload_1 // push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end
操作數棧 的數據讀取、寫入就是出棧和如棧操作。
每個棧幀都包含一個指向運行時常量池的引用,持有這個引用是為了支持動態連接。
符號池的引用,有一部分是在第一次使用或者初始化的時候就確定下來,這個稱為靜態引用。
還有一部分是在每次執行的時候采取確定,這個就是動態連接。
方法只有2中退出方式,正常情況下,遇到return指令退出。還有就是異常退出。
正常情況:一般情況下,棧幀會保存 在程序計數器中的調用者的地址。虛擬機通過這個方式,執行方法調用者的地址,
然後把返回值壓入調用者中的操作數棧。
異常情況:方法不會返回任何值,返回地址有異常表來確定,棧幀一般不存儲信息。
方法調用階段不是執行該方法,而僅僅時確認要調用那個方法。class文件在編譯階段沒有連接這一過程,、
所以動態連接這個在C++就已經有的技術,在java運用到了一個新的高度。所有的函數(除了私有方法,構造方法 & 靜態方法,下同),理論上
都可以時C++裡面的虛函數。所以所有的函數都需要通過動態綁定來確定“明確”的函數實體。
所有方法調用的目標方法都是常量池中的符號引用。在類的加載解析階段,會將一部分目標方法轉化為直接引用。(可以理解為具體方法的直接地址)
可以轉化的方法,主要為靜態方法 & 私有方法。
Java虛擬機提供5中方法調用命令:
invokestatic:調用靜態方法
invokespecial:調用構造器,私有方法和父類方法
invokevirtual:調用虛方法
invokeinterface:調用接口方法
invokedynamic:現在運行時動態解析出該方法,然後執行。
invokestatic & invokespecial 對應的方法,都是在加載解析後,可以直接確定的。所以這些方法為非虛方法。
java規定 final修飾的是一種非虛方法。
靜態分派
先看一個例子:
package com.joyfulmath.jvmexample.dispatch; import com.joyfulmath.jvmexample.TraceLog; /** * @author deman.lu * @version on 2016-05-19 13:53 */ public class StaticDispatch { static abstract class Human{ } static class Man extends Human{ } static class Woman extends Human{ } public void sayHello(Human guy) { TraceLog.i("Hello guy!"); } public void sayHello(Man man) { TraceLog.i("Hello gentleman!"); } public void sayHello(Woman man) { TraceLog.i("Hello lady!"); } public static void action() { Human man = new Man(); Human woman = new Woman(); StaticDispatch dispatch = new StaticDispatch(); dispatch.sayHello(man); dispatch.sayHello(woman); } }
05-19 13:58:05.538 14881-14881/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)] 05-19 13:58:05.539 14881-14881/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)]
結果執行了public void sayHello(Human guy)函數。這不是應該多態嗎?
Human man = new Man();
這裡的Human我們理解為靜態類型,後面的Man是實際類型。我們在編譯器只知道靜態類型,後面的實際類型等到動態連接的時候才知道。
所以對於sayHello方法,虛擬機在重載時,是通過參數的靜態類型,而不是實際類型來判斷使用那個方法的。
如果對類型做強制轉換:
public static void action() { Human man = new Man(); Human woman = new Woman(); StaticDispatch dispatch = new StaticDispatch(); dispatch.sayHello(man); dispatch.sayHello(woman); dispatch.sayHello((Man)man); dispatch.sayHello((Woman)woman); } 05-19 14:08:29.000 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)] 05-19 14:08:29.001 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)] 05-19 14:08:29.001 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello gentleman! [at (StaticDispatch.java:29)] 05-19 14:08:29.002 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello lady! [at (StaticDispatch.java:34)]
如果強轉了以後,類型也跟著變化了。
靜態分配的典型應用是方法重載。但是方法重載有時候不是唯一的,所以只能選合適的。
比如:
public void sayHello(int data) { TraceLog.i("Hello int!"); } public void sayHello(long data) { TraceLog.i("Hello long"); }
當sayHello(1)的時候,一般情況下會調用int型的方法,但是如果注釋調,只有long型的方法,long型參數方法就會被調用。
動態分派
上面講的是重載,這裡是重寫(@Override)
package com.joyfulmath.jvmexample.dispatch; import com.joyfulmath.jvmexample.TraceLog; /** * @author deman.lu * @version on 2016-05-19 14:26 */ public class DynamicDispatch { static abstract class Human{ protected abstract void sayHello(); } static class Man extends Human{ @Override protected void sayHello() { TraceLog.i("Hello gentleman!"); } } static class Woman extends Human{ @Override protected void sayHello() { TraceLog.i("Hello lady!"); } } public static void action() { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); man = new Woman(); man.sayHello(); } }
先來看上面標紅的這句:方法要解析man 的sayhello,問題是man是什麼東西,我在解析的時候,是不知道的。所以“man.sayHello();”具體執行的那個類的方法,是需要在虛擬機
動態連接的時候才知道,這個就是多態。如果使用javap分析就可以知道這句話,在class文件裡面是ynamicDispatch$Human: sayHello. 是的class文件不知道這個sayhello到底要去
調哪個方法。
invokevirtual指令解析的過程大概如下:首先在操作數棧裡第一個元素的實際類型,即為C。
如果在類型C中找到與常量描述符相同的類名和方法,則權限校驗通過後,即為找到該法方法,則返回這個方法的直接引用。
否則,對C的父類進行依次查找。
這個過程通俗一點就是,先從當前類裡面尋找“同名”的該方法,如果沒有,就從C的父類裡面找,知道找到為止!
這個找到的方法,就是我們實際要調的方法。
如果找不到,就是exception。一般情況下,編譯工具會幫我們避免這種情況。
單分派和多分派
概念上理解比較麻煩,說白了一點就是重載和重寫都存在的情況:
package com.joyfulmath.jvmexample.dispatch; import com.joyfulmath.jvmexample.TraceLog; /** * @author deman.lu * @version on 2016-05-19 15:02 */ public class MultiDispatch { static class QQ{} static class _360{} public static class Father{ public void hardChoice(QQ qq){ TraceLog.i("Father QQ"); } public void hardChoice(_360 aa){ TraceLog.i("Father 360"); } } public static class Son extends Father{ public void hardChoice(QQ qq){ TraceLog.i("Son QQ"); } public void hardChoice(_360 aa){ TraceLog.i("Son 360"); } } public static void action() { Father father = new Father(); Father son = new Son(); father.hardChoice(new _360()); son.hardChoice(new QQ()); } }
05-19 15:07:44.429 29011-29011/com.joyfulmath.jvmexample I/MultiDispatch$Father: hardChoice: Father 360 [at (MultiDispatch.java:19)] 05-19 15:07:44.429 29011-29011/com.joyfulmath.jvmexample I/MultiDispatch$Son: hardChoice: Son QQ [at (MultiDispatch.java:25)]
結果沒有任何懸念,但是過程還是需要明確的。hardChoice的選擇是在靜態編譯的時候就確認的。
而son.hardchoise 已經確認了函數的類型,只是需要進一步確認實體類型。所以動態連接是單分派。
動態語言支持:
使用C++語言可以定義一個調用方法:
void sort(int list[],const int size,int (*compare)(int,int));
但是java很難做到這一點,
void sort(List list,Compare c);Compare 一般要用接口實現。
在java 1.7 有一種方法可以支持該功能 MethodHandle。
這部分內容,由於我本地環境無法配置還調用,將會再後續更新。
鋪墊了這麼多,下面來講講字節碼的執行
基於棧的指令集 和基於寄存器的指令集。
先看一個加法過程:
iconst_1
iconst_1
iadd
istore_0
這是基於棧的,也就是上文說的操作數棧。
先把2個元素要入棧,然後相加,放回棧頂,然後把棧頂的值存在slot 0裡面。
基於寄存器的就不解釋了。
基於寄存器 和基於棧的指令集現在都存在。所以很難說孰優孰劣。
基於棧的指令集 是和硬件無關的,而基於寄存器則依賴於硬件基礎。基於寄存器在效率上優勢。
但是虛擬機的出現,就是為了提供跨平台的支持,所以jvm的執行引擎是基於棧的指令集。
public int calc() { int a = 100; int b = 200; int c = 300; return (a+b)*c; }
以下是javap的分析結果:
以下圖片描述了整個執行過程中代碼,操作數棧,& 局部變量表的變化。
這些過程只是一個概念模型,實際虛擬機會有很多優化的情況。
聲明:本文相關圖片來之參考書面,相關版權歸原作者所有。
參考:
《深入理解java虛擬機》 周志明