閱讀624 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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

理解在Java虛擬機中Java代碼如何別被編譯成字節碼並執行是非常重要的,因為這可以幫助你理解你的程序在運行時發生了什麼。這種理解不僅能確保你對語言特性有邏輯上的認識而且做具體的討論時可以理解在語言特性上的妥協和副作用。

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

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

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

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

變量

局部變量

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,它將一個操作數放到本地變量數組中合適的位置。

上麵的代碼在內存中執行的情況如下:

java_local_veribale_

這個類文件中對應每一個方法還包含一個本地便變量表(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

ableswitch指令擁有值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 內部原理

 

最後更新:2017-05-22 16:39:26

  上一篇:go  為什麼我的JVM能實際使用的內存比-Xmx指定的少?
  下一篇:go  Linux內核的內存屏障