Java代碼到字節碼——第一部分

 

理解在Java虛擬機中Java代碼如何別被編譯成字節碼並執行是很是重要的,由於這能夠幫助你理解你的程序在運行時發生了什麼。這種理解不只能確保你對語言特性有邏輯上的認識並且作具體的討論時能夠理解在語言特性上的妥協和反作用。html

這篇文章講解了在Java虛擬機上Java代碼是如何編譯成字節碼並執行的。想了解JVM內部架構和在字節碼執行期間不一樣內存區域之間的差別能夠查看個人上一篇文章 JVM 內部原理java

這篇文章共分爲三個部分,每一個部分被劃分爲幾個小節。你能夠單獨的閱讀某一部分,不過你能夠閱讀該部分快速瞭解一些基本的概念。每個部分將會包含不一樣的Java字節碼指令而後解釋它們圖和被編譯並做爲字節碼指令被執行的,目錄以下:git

  • 第一部分-基本編程概念
    • 變量
      • 局部變量
      • 成員變量
      • 常量
      • 靜態變量
    • 條件語句
      • if-else
      • switch
    • 循環語句
      • while循環
      • for循環
      • do-while循環
  • 第二部分-面向對象和安全
    • try-catch-finally
    • synchronized
    • 方法調用
    • new (對象和數組)
  • 第三部分-元編程
    • 泛型
    • 註解
    • 反射

這篇文章包含很代碼示例和生成的對應字節碼。在字節碼中每條指令(或操做碼)前面的數字指示了這個字節的位置。好比一條指令如1: iconst_1 僅一個字節的長度,沒有操做數,因此,接下來的字節碼的位置爲2。再好比這樣一條指令1: bipush 5將會佔兩個字節,操做碼bipush佔一個字節,操做數5佔一個字節。在這個示例中,接下來的字節碼的位置爲3,由於操做數佔用的字節在位置2。github

變量

局部變量

Java虛擬機是基於棧的架構。當一個方法包括初始化main方法執行,在棧上就會建立一個棧幀(frame),棧幀中存放着方法中的局部變量。局部變量數組(local veriable array)包含在方法執行期間用到的全部變量包括一個引用變量this,全部的方法參數和在方法體內定義的變量。對於類方法(好比:static方法)方法參數從0開始,然而,對於實例方法,第0個slot用來存放this。算法

一個局部變量類型能夠爲:編程

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

除了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。

上面的代碼在內存中執行的狀況以下:

java_class_variable_creation_byte_code

 

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

下面的代碼展現了一條簡單的用來比較兩個整數大小的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的字節碼。

java_if_else_byte_code

下面的代碼示例展現了一個稍微複雜點的例子,須要一個兩步比較:

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語句來返回。

java_if_else_byte_code_extra_goto

和存在進行數值比較的操做碼同樣,也有進行引用相等性比較的操做碼好比==,與null進行比較好比 == null和 != null,測試一個對象的類型好比 instanceof。

if_cmp eq ne lt le gt ge 這組操做碼用於操做數棧棧頂的兩個整數並跳轉到一個新的字節碼處。可取的值有:

  • eq – 等於
  • ne – 不等於
  • lt – 小於
  • le – 小於或等於
  • gt – 大於
  • ge – 大於或等於

if_acmp eq ne  這兩個操做碼用於測試兩個引用相等(eq)仍是不相等(ne),而後跳轉到由操做數指定的新一個新的字節碼處。

ifnonnull/ifnull 這兩個字節碼用於測試兩個引用是否爲null或者不爲null,而後跳轉到由操做數指定的新一個新的字節碼處。

lcmp 這個操做碼用於比較在操做數棧棧頂的兩個整數,而後將一個值推送到操做數棧,以下所示:

  • 若是 value1 > value2 -> 推送1
  • 若是 value1 = value2 -> 推送0
  • 若是 value1 < value2 -> 推送-1

fcmp l g / dcmp l g 這組操做碼用於比較兩個float或者double值,而後將一個值推送的操做數棧,以下所示:

  • 若是 value1 > value2 -> 推送1
  • 若是 value1 = value2 -> 推進0
  • 若是value1 < value2 -> 推送-1

以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進行比較,而後跳轉到操做數指定位置的字節碼處。若是比較成功,這些指令老是被用於更復雜的,不能用一條指令完成的條件邏輯,例如,測試一個方法調用的結果。

switch

一個Java switch表達式容許的類型能夠爲char,byte,short,int,Character,Byte,Short.Integer,String或者一個enum類型。爲了支持switch語句,Java虛擬機使用兩個特殊的指令:tableswitchlookupswitch,它們背後都是經過整數值來實現的。僅使用整數值並不會出現什麼問題,由於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處的字節碼。 下圖展現了這個字節碼是如何執行的:

java_switch_tableswitch_byte_code

若是在case語句中的值」離得太遠「(好比太稀疏),這種方法就會不太可取,由於它會佔用太多的內存。當switch中case比較稀疏時,可使用lookupswitch來替代tableswitchlookupswitch會爲每個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會提供匹配值個數並對匹配值進行排序。下圖顯示了上述代碼是如何被執行的:

java_switch_lookupswitch_byte_code

String switch

在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」時代碼是如何執行的:

java_string_switch_byte_code_1

java_string_switch_byte_code_2

java_string_switch_byte_code_3

若是不一樣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循環

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。

java_while_loop_byte_code_1

java_while_loop_byte_code_2

for循環

for循環和while循環在字節碼層面使用了徹底相同的模式。這並不使人驚訝由於全部的while循環均可以用一個相同的for循環來重寫。上面那個簡單的的while循環的例子能夠用一個for循環來重寫,併產生徹底同樣的字節碼,以下所示:

public void forLoop() {
    for(int i = 0; i < 2; i++) {

    }
}

do-while循環

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

java_do_while_loop_byte_code_1

java_do_while_loop_byte_code_2

更多文章

下面兩篇文章將會包含下列主體:

  • 第二部分 – 面向對象和安全(下篇文章)
    • try-catch-finally
    • synchronized
    • 方法條用(和參數)
    • new (對象和數組)
  • 第三部分 – 元編程
    • 泛型
    • 註解
    • 反射 瞭解更多關於虛擬機內部架構和字節碼運行期間不一樣的內存區域能夠查看個人上篇文章 JVM 內部原理
相關文章
相關標籤/搜索