理解在Java虛擬機中Java代碼如何別被編譯成字節碼並執行是很是重要的,由於這能夠幫助你理解你的程序在運行時發生了什麼。這種理解不只能確保你對語言特性有邏輯上的認識並且作具體的討論時能夠理解在語言特性上的妥協和反作用。html
這篇文章講解了在Java虛擬機上Java代碼是如何編譯成字節碼並執行的。想了解JVM內部架構和在字節碼執行期間不一樣內存區域之間的差別能夠查看個人上一篇文章 JVM 內部原理。java
這篇文章共分爲三個部分,每一個部分被劃分爲幾個小節。你能夠單獨的閱讀某一部分,不過你能夠閱讀該部分快速瞭解一些基本的概念。每個部分將會包含不一樣的Java字節碼指令而後解釋它們圖和被編譯並做爲字節碼指令被執行的,目錄以下:git
這篇文章包含很代碼示例和生成的對應字節碼。在字節碼中每條指令(或操做碼)前面的數字指示了這個字節的位置。好比一條指令如1: iconst_1
僅一個字節的長度,沒有操做數,因此,接下來的字節碼的位置爲2。再好比這樣一條指令1: bipush 5
將會佔兩個字節,操做碼bipush
佔一個字節,操做數5佔一個字節。在這個示例中,接下來的字節碼的位置爲3,由於操做數佔用的字節在位置2。github
Java虛擬機是基於棧的架構。當一個方法包括初始化main方法執行,在棧上就會建立一個棧幀(frame),棧幀中存放着方法中的局部變量。局部變量數組(local veriable array)包含在方法執行期間用到的全部變量包括一個引用變量this,全部的方法參數和在方法體內定義的變量。對於類方法(好比:static方法)方法參數從0開始,然而,對於實例方法,第0個slot用來存放this。算法
一個局部變量類型能夠爲:編程
除了long和double全部的類型在本地變量數組中佔用一個slot,long和double須要兩個連續的slot由於這兩個類型爲64位類型。數組
當在操做數棧上建立一個新的變量來存放一個這個新變量的值。這個新變量的值隨後會被存放到本地變量數組對應的位置上。若是這個變量不是一個基本類型,對應的slot上值存放指向這個變量的引用。這個引用指向存放在堆中的一個對象。安全
例如:數據結構
int i = 5;架構
被編譯爲字節碼爲:
0: bipush 5 2: istore_0
bipush:
將一個字節做爲一個整數推送到操做數棧。在這個例子中5被推送到操做數棧。
istore_0:
它是一組格式爲istore_操做數的其中之一,它們都是將一個整數存儲到本地變量。n爲在本地變量數組中的位置,取值只能爲0,1,2,或者3。另外一個操做碼用做值大於3的狀況,爲istore
,它將一個操做數放到本地變量數組中合適的位置。
上面的代碼在內存中執行的狀況以下:
這個類文件中對應每個方法還包含一個本地便變量表(local veribale table),若是這段代碼被包含在一個方法中,在類文件對應於這個方法的本地變量表中你將會獲得下面的實體(entry):
LocalVariableTable: Start Length Slot Name Signature 0 1 1 i I
一個成員變量(field)被做爲一個類實例(或對象)的一部分存儲在堆上。關於這個成員變量的信息被存放在類文件中field_info
數組中,以下:
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info contant_pool[constant_pool_count – 1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
另外,若是這個變量被初始化,進行初始化操做的字節碼將被添加到構造器中。
當以下的代碼被編譯:
public class SimpleClass{ public int simpleField = 100; }
一個額外的小結將會使用javap命令來演示將成員變量添加到field_info
數組中。
public int simpleField; Signature: I flags: ACC_PUBLIC
進行初始化操做的字節碼被添加到構造器中,以下:
public SimpleClass(); Signature: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: bipush 100 7: putfield #2 // Field simpleField:I 10: return
aload_0: 將本地變量數組slot中一個對象引用推送到操做數棧棧頂。儘管,上面的代碼中顯示沒有構造器對成員變量進行初始化,實際上,編譯器會建立一個默認的構造器對成員變量進行初始化。所以,第一個局部變量實際上指向this,所以,aload_0
操做碼將this這個引用變量推送到操做數棧。aload_0
是一組格式爲aload_的操做數中其中一員,它們的做用都是將一個對象引用推送到操做數棧。其中n指的是被訪問的本地變量數組中這個對象引用所在的位置,取值只能爲0,1,2或3。與之相似的操做碼有iload_,lload_,fload_和dload_,不過這些操做碼是用來加載值而不是一個對象引用,這裏的i指的是int,l指的是long,f指的是float,d指的是double。本地變量的索引大於3的可使用iload,lload,fload,dload和aload來加載,這些操做碼都須要一個單個的操做數指定要加載的本地變量的索引。
invokespecial: invokespecial指令用來調用實例方法,私有方法和當前類的父類的方法。它是一組用來以不一樣的方式調用方法的操做碼的一部分,包括,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual。invokespecial指令在這段代碼用來調用父類的構造器。
bipush: 將一個字節做爲一個整數推送到操做數棧。在這個例子中100被推送到操做數棧。
putfield: 後面跟一個操做數,這個操做數是運行時常量池中一個成員變量的引用,在這個例子中這個成員變量叫作simpleField。給這個成員變量賦值,而後包含這個成員變量的對象一塊兒被彈出操做數棧。前面的aload_0指令將包含這個成員變量的對象和前面的bipush指令將100分別推送到操做數棧頂。putfield隨後將它們都從操做數棧頂移除(彈出)。最終結果就是在這個對象上的成員變量simpleFiled的值被更新爲100。
上面的代碼在內存中執行的狀況以下:
putfield操做碼有一個單個的操做數指向在常量池中第二個位置。JVM維護了一個常量池,一個相似於符號表的運行時數據結構,可是包含了更多的數據。Java中的字節碼須要數據,一般因爲這種數據太大而不能直接存放在字節碼中,而是放在常量池中,字節碼中持有一個指向常量池中的引用。當一個類文件被建立時,其中就有一部分爲常量池,以下所示:
Constant pool: #1 = Methodref #4.#16 // java/lang/Object."<init>":()V #2 = Fieldref #3.#17 // SimpleClass.simpleField:I #3 = Class #13 // SimpleClass #4 = Class #19 // java/lang/Object #5 = Utf8 simpleField #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 SimpleClass #14 = Utf8 SourceFile #15 = Utf8 SimpleClass.java #16 = NameAndType #7:#8 // "<init>":()V #17 = NameAndType #5:#6 // simpleField:I #18 = Utf8 LSimpleClass; #19 = Utf8 java/lang/Object
被final修飾的變量咱們稱之爲常量,在類文件中咱們標識爲ACC_FINAL
。
例如:
public class SimpleClass { public final int simpleField = 100; }
變量描述中多了一個ACC_FINAL
參數:
public static final int simpleField = 100; Signature: I flags: ACC_PUBLIC, ACC_FINAL ConstantValue: int 100
不過,構造器中的初始化操做並無受影響:
4: aload_0 5: bipush 100 7: putfield #2 // Field simpleField:I
被static修飾的變量,咱們稱之爲靜態類變量,在類文件中被標識爲ACC_STATIC
,以下所示:
public static int simpleField; Signature: I flags: ACC_PUBLIC, ACC_STATIC
在實例構造器中並無發現用來對靜態變量進行初始化的字節碼。靜態變量的初始化是在類構造器中,使用putstatic
操做碼而不是putfield
字節碼,是類構造器的一部分。
static {}; Signature: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: bipush 100 2: putstatic #2 // Field simpleField:I 5: return
條件流控制,好比,if-else語句和switch語句,在字節碼層面都是經過使用一條指令來與其它的字節碼比較兩個值和分支。
for循環和while循環這兩條循環語句也是使用相似的方式來實現的,不一樣的是它們一般還包含一條goto指令,來達到循環的目的。do-while循環不須要任何goto指令由於他們的條件分支位於字節碼的尾部。更多的關於循環的細節能夠查看 loops section。
一些操做碼能夠比較兩個整數或者兩個引用,而後在一個單條指令中執行一個分支。其它類型之間的比較如double,long或float須要分爲兩步來實現。首先,進行比較後將1,0或-1推送到操做數棧頂。接下來,基於操做數棧上值是大於,小於仍是等於0執行一個分支。
首先,咱們拿if-else語句爲例進行講解,其餘用來進行分支跳轉的不一樣的類型的指令將會被包含在下面的講解之中。
下面的代碼展現了一條簡單的用來比較兩個整數大小的if-else語句。
public int greaterThen(int intOne, int intTwo) { if (intOne > intTwo) { return 0; } else { return 1; } }
這個方法編譯成以下的字節碼:
0: iload_1 1: iload_2 2: if_icmple 7 5: iconst_0 6: ireturn 7: iconst_1 8: ireturn
首先,使用iload_1和iload_2將兩個參數推送到操做數棧。而後,使用if_icmple比較操做數棧棧頂的兩個值。若是intOne小於或等於intTwo,這個操做數分支變成字節碼7。注意,在Java代碼中if條件中的測試與在字節碼中是徹底相反的,由於在字節碼中若是if條件語句中的測試成功執行,則執行else語句塊中的內容,而在Java代碼,若是if條件語句中的測試成功執行,則執行if語句塊中的內容。換句話說,if_icmple指令是在測試若是if條件不爲true,則跳過if代碼塊。if代碼塊的主體是序號爲5和6的字節碼,else代碼塊的主體是序號爲7和8的字節碼。
下面的代碼示例展現了一個稍微複雜點的例子,須要一個兩步比較:
public int greaterThen(float floatOne, float floatTwo) { int result; if (floatOne > floatTwo) { result = 1; } else { result = 2; } return result; }
這個方法產生以下的字節碼:
0: fload_1 1: fload_2 2: fcmpl 3: ifle 11 6: iconst_1 7: istore_3 8: goto 13 11: iconst_2 12: istore_3 13: iload_3 14: ireturn
在這個例子中,首先使用fload_1和fload_2將兩個參數推送到操做數棧棧頂。這個例子與上一個例子不一樣在於這個須要兩步比較。fcmpl首先比較floatOne和floatTwo,而後將結果推送到操做數棧棧頂。以下所示:
floatOne > floatTwo -> 1 floatOne = floatTwo -> 0 floatOne < floatTwo -> -1 floatOne or floatTwo= Nan -> 1
接下來,若是fcmpl的結果是<=0,ifle用來跳轉到索引爲11處的字節碼。
這個例子和上一個例子的不一樣之處還在於這個方法的尾部只有一個單個的return語句,而在if語句塊的尾部還有一條goto指令用來防止else語句塊被執行。goto分支對應於序號爲13處的字節碼iload_3,用來將本地變量表中第三個slot中存放的結果推送掃操做數棧頂,這樣就能夠由retrun語句來返回。
和存在進行數值比較的操做碼同樣,也有進行引用相等性比較的操做碼好比==,與null進行比較好比 == null和 != null,測試一個對象的類型好比 instanceof。
if_cmp eq ne lt le gt ge 這組操做碼用於操做數棧棧頂的兩個整數並跳轉到一個新的字節碼處。可取的值有:
if_acmp eq ne 這兩個操做碼用於測試兩個引用相等(eq)仍是不相等(ne),而後跳轉到由操做數指定的新一個新的字節碼處。
ifnonnull/ifnull 這兩個字節碼用於測試兩個引用是否爲null或者不爲null,而後跳轉到由操做數指定的新一個新的字節碼處。
lcmp 這個操做碼用於比較在操做數棧棧頂的兩個整數,而後將一個值推送到操做數棧,以下所示:
fcmp l g / dcmp l g 這組操做碼用於比較兩個float或者double值,而後將一個值推送的操做數棧,以下所示:
以l或g類型操做數結尾的差異在於它們如何處理NaN。fcmpg和dcmpg將int值1推送到操做數棧而fcmpl和dcmpl將-1推送到操做數棧。這就確保了在測試時若是兩個值中有一個爲NaN(Not A Number),測試就不會成功。好比,若是x > y(這裏x和y都爲doube類型),x和y中若是有一個爲NaN,fcmpl指令就會將-1推送到操做數棧。接下來的操做碼總會是一個ifle指令,若是這是棧頂的值小於0,就會發生分支跳轉。結果,x和y中有一個爲NaN,ifle就會跳過if語句塊,防止if語句塊中的代碼被執行到。
instanceof 若是操做數棧棧頂的對象一個類的實例,這個操做碼將一個int值1推送到操做數棧。這個操做碼的操做數用來經過提供常量池中的一個索引來指定類。若是這個對象爲null或者不是指定類的實例則int值0就會被推送到操做數棧。
if eq ne lt le gt ge 全部的這些操做碼都是用來將操做數棧棧頂的值與0進行比較,而後跳轉到操做數指定位置的字節碼處。若是比較成功,這些指令老是被用於更復雜的,不能用一條指令完成的條件邏輯,例如,測試一個方法調用的結果。
一個Java switch表達式容許的類型能夠爲char,byte,short,int,Character,Byte,Short.Integer,String或者一個enum類型。爲了支持switch語句,Java虛擬機使用兩個特殊的指令:tableswitch
和lookupswitch
,它們背後都是經過整數值來實現的。僅使用整數值並不會出現什麼問題,由於char,byte,short和enum類型均可以在內部被提高爲int類型。在Java7中添加對String的支持,背後也是經過整數來實現的。tableswitch
經過速度更快,可是一般佔用更多的內存。tableswitch
經過列舉在最小和最大的case值之間全部可能的case值來工做。最小和最大值也會被提供,因此若是switch變量不在列舉的case值的範圍以內,JVM就會當即跳到default語句塊。在Java代碼沒有提供的case語句的值也會被列出,不過指向default語句塊,確保在最小值和最大值之間的全部值都會被列出來。例如,執行下面的swicth語句:
public int simpleSwitch(int intOne) { switch (intOne) { case 0: return 3; case 1: return 2; case 4: return 1; default: return -1; }
這段代碼產生以下的字節碼:
0: iload_1 1: tableswitch { default: 42 min: 0 max: 4 0: 36 1: 38 2: 42 3: 42 4: 40 } 36: iconst_3 37: ireturn 38: iconst_2 39: ireturn 40: iconst_1 41: ireturn 42: iconst_m1 43: ireturn
tableswitch
指令擁有值0,1和4去匹配Java代碼中提供的case語句,每個值指向它們對應的代碼塊的字節碼。tableswitch
指令還存在值2和3,它們並無在Java代碼中做爲case語句提供,它們都指向default代碼塊。當這些指令被執行時,在操做數棧棧頂的值會被檢查看是否在最大值和最小值之間。若是值不在最小值和最大值之間,代碼執行就會跳到default分支,在上面的例子中它位於序號爲42的字節碼處。爲了確保default分支的值能夠被tableswitch指令發現,因此它老是位於第一個字節處(在任何須要的對齊補白以後)。若是值位於最小值和最大值之間,就用於索引tableswitch
內部,尋找合適的字節碼進行分支跳轉。例如,值爲,則代碼執行會跳轉到序號爲38處的字節碼。 下圖展現了這個字節碼是如何執行的:
若是在case語句中的值」離得太遠「(好比太稀疏),這種方法就會不太可取,由於它會佔用太多的內存。當switch中case比較稀疏時,可使用lookupswitch
來替代tableswitch
。lookupswitch
會爲每個case語句例舉出分支對應的字節碼,可是不會列舉出全部可能的值。當執行lookupswitch
時,位於操做數棧棧頂的值會同lookupswitch
中的每個值進行比較,從而決定正確的分支地址。使用lookupswitch
,JVM會查找在匹配列表中查找正確的匹配,這是一個耗時的操做。而使用tableswitch
,JVM能夠快速定位到正確的值。當一個選擇語句被編譯時,編譯器必須在內存和性能兩者之間作出權衡,決定選擇哪種選擇語句。下面的代碼,編譯器會使用lookupswitch:
public int simpleSwitch(int intOne) { switch (intOne) { case 10: return 1; case 20: return 2; case 30: return 3; default: return -1; } }
這段代碼產生的字節碼,以下:
0: iload_1 1: lookupswitch { default: 42 count: 3 10: 36 20: 38 30: 40 } 36: iconst_1 37: ireturn 38: iconst_2 39: ireturn 40: iconst_3 41: ireturn 42: iconst_m1 43: ireturn
爲了更高效的搜索算法(比線性搜索更高效),lookupswitch
會提供匹配值個數並對匹配值進行排序。下圖顯示了上述代碼是如何被執行的:
在Java7中,switch語句增長了對字符串類型的支持。雖然現存的實現switch語句的操做碼僅支持int類型且沒有新的操做碼加入。字符串類型的switch語句分爲兩個部分完成。首先,比較操做數棧棧頂和每一個case語句對應的值之間的哈希值。這一步能夠經過lookupswitch
或者tableswitch
來完成(取決於哈希值的稀疏度)。這也會致使一個分支對應的字節碼去調用String.equals()進行一次精確地匹配。一個tableswitch
指令將利用String.equlas()的結果跳轉到正確的case語句的代碼處。
public int simpleSwitch(String stringOne) { switch (stringOne) { case "a": return 0; case "b": return 2; case "c": return 3; default: return 4; } }
這個字符串switch語句將會產生以下的字節碼:
0: aload_1 1: astore_2 2: iconst_m1 3: istore_3 4: aload_2 5: invokevirtual #2 // Method java/lang/String.hashCode:()I 8: tableswitch { default: 75 min: 97 max: 99 97: 36 98: 50 99: 64 } 36: aload_2 37: ldc #3 // String a 39: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 42: ifeq 75 45: iconst_0 46: istore_3 47: goto 75 50: aload_2 51: ldc #5 // String b 53: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 56: ifeq 75 59: iconst_1 60: istore_3 61: goto 75 64: aload_2 65: ldc #6 // String c 67: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 70: ifeq 75 73: iconst_2 74: istore_3 75: iload_3 76: tableswitch { default: 110 min: 0 max: 2 0: 104 1: 106 2: 108 } 104: iconst_0 105: ireturn 106: iconst_2 107: ireturn 108: iconst_3 109: ireturn 110: iconst_4 111: ireturn
這個類包含這段字節碼,同時也包含下面由這段字節碼引用的常量池值。瞭解更多關於常量池的知識能夠查看JVM內部原理這篇文章的 運行時常量池 部分。
Constant pool: #2 = Methodref #25.#26 // java/lang/String.hashCode:()I #3 = String #27 // a #4 = Methodref #25.#28 // java/lang/String.equals:(Ljava/lang/Object;)Z #5 = String #29 // b #6 = String #30 // c #25 = Class #33 // java/lang/String #26 = NameAndType #34:#35 // hashCode:()I #27 = Utf8 a #28 = NameAndType #36:#37 // equals:(Ljava/lang/Object;)Z #29 = Utf8 b #30 = Utf8 c #33 = Utf8 java/lang/String #34 = Utf8 hashCode #35 = Utf8 ()I #36 = Utf8 equals #37 = Utf8 (Ljava/lang/Object;)Z
注意,執行這個switch須要的字節碼的數量包括兩個tableswitch
指令,幾個invokevirtual
指令去調用 String.equals()。瞭解更多關於invokevirtual
的更多細節能夠參看下篇文章方法調用的部分。下圖顯示了在輸入「b」時代碼是如何執行的:
若是不一樣case匹配到的哈希值相同,好比,字符串」FB」和」Ea」的哈希值都是28。這能夠經過像下面這樣輕微的調整equlas方法流來處理。注意,序號爲34處的字節碼:ifeg 42
去調用另外一個String.equals() 來替換上一個不存在哈希衝突的例子中的 lookupsswitch
操做碼。
public int simpleSwitch(String stringOne) { switch (stringOne) { case "FB": return 0; case "Ea": return 2; default: return 4; } }
上面代碼產生的字節碼以下:
0: aload_1 1: astore_2 2: iconst_m1 3: istore_3 4: aload_2 5: invokevirtual #2 // Method java/lang/String.hashCode:()I 8: lookupswitch { default: 53 count: 1 2236: 28 } 28: aload_2 29: ldc #3 // String Ea 31: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 34: ifeq 42 37: iconst_1 38: istore_3 39: goto 53 42: aload_2 43: ldc #5 // String FB 45: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 48: ifeq 53 51: iconst_0 52: istore_3 53: iload_3 54: lookupswitch { default: 84 count: 2 0: 80 1: 82 } 80: iconst_0 81: ireturn 82: iconst_2 83: ireturn 84: iconst_4 85: ireturn
條件流控制,好比,if-else語句和switch語句都是經過使用一條指令來比較兩個值而後跳轉到相應的字節碼來實現的。瞭解更多關於條件語句的細節能夠查看 conditionals section 。
循環包括for循環和while循環也是經過相似的方法來實現的除了它們一般一個goto指令來實現字節碼的循環。do-while循環不須要任何goto指令,由於它們的條件分支位於字節碼的末尾。
一些字節碼能夠比較兩個整數或者兩個引用,而後使用一個單個的指令執行一個分支。其餘類型之間的比較如double,long或者float須要兩步來完成。首先,執行比較,將1,0,或者-1 推送到操做數棧棧頂。接下來,基於操做數棧棧頂的值是大於0,小於0仍是等於0執行一個分支。瞭解更多關於進行分支跳轉的指令的細節能夠 see above 。
while循環一個條件分支指令好比 if_fcmpge
或 if_icmplt
(如上所述)和一個goto語句。在循環事後就理解執行條件分支指令,若是條件不成立就終止循環。循環中最後一條指令是goto,用於跳轉到循環代碼的起始處,直到條件分支不成立,以下所示:
public void whileLoop() { int i = 0; while (i < 2) { i++; } }
被編譯成:
0: iconst_0 1: istore_1 2: iload_1 3: iconst_2 4: if_icmpge 13 7: iinc 1, 1 10: goto 2 13: return
if_cmpge
指令測試在位置1處的局部變量是否等於或者大於10,若是大於10,這個指令就跳到序號爲14的字節碼處完成循環。goto指令保證字節碼循環直到if_icmpge
條件在某個點成立,循環一旦結束,程序執行分支當即就會跳轉到return
指令處。iinc
指令是爲數很少的在操做數棧上不用加載(load)和存儲(store)值能夠直接更新一個局部變量的指令之一。在這個例子中,iinc
將第一個局部變量的值加 1。
for循環和while循環在字節碼層面使用了徹底相同的模式。這並不使人驚訝由於全部的while循環均可以用一個相同的for循環來重寫。上面那個簡單的的while循環的例子能夠用一個for循環來重寫,併產生徹底同樣的字節碼,以下所示:
public void forLoop() { for(int i = 0; i < 2; i++) { } }
do-while循環和for循環以及while循環也很是的類似,除了它們不須要將goto指令做爲條件分支成爲最後一條指令用於回退到循環起始處。
public void doWhileLoop() { int i = 0; do { i++; } while (i < 2); }
產生的字節碼以下:
0: iconst_0 1: istore_1 2: iinc 1, 1 5: iload_1 6: iconst_2 7: if_icmplt 2 10: return
下面兩篇文章將會包含下列主體: