从Java代码到字节码(1)

理解Java代码是如何被编译为字节码并在Java虚拟机(JVM)上执行是非常重要的,这将帮助理解你的程序是如何执行的。这样的理解不仅仅能够让你在逻辑上更好的掌握语言特性,而且能够有机会理解在做出重要决定时所需的权衡以及相应的副作用。

这篇文章解释了Java代码是如何被编译为字节码并在JVM上执行的,如果想要理解JVM的内部结构和以及字节码在运行过程中占用的不同的内存区域,请看我之前的深入JVM一文。

这篇文章被分为了3个部分,每个部分又分为若干个章节。你可以单独的阅读任何一个章节,但是如果按顺序来阅读,更容易建立起完整的概念体系。每个章节都会讲解Java代码结构的不同部分并解释各部分都是如何编译为字节码的,具体章节如下:

目录

这篇文章包含了很多的例子,展示了这些例子所对应生成的典型的字节码。字节码中在每条指令或者操作码之前的数字标识了字节的位置。举个例子,指令1: iconst_1说明该指令由于没有操作数,所以只有1个字节的长度,因此接下来的字节码就在位置2;指令1: bipushu 5就会占用两个字节,一个字节用于存储操作码bipush,另一个存储操作数5,这种情况下,因为操作数占用了位置2,所以下一条指令就会从位置3开始。

 

变量

 

局部变量

Java虚拟机(JVM)是基于栈结构的。对于最初的main方法产生的所有的方法调用,都会在栈中产生一个帧,这些帧各自包含一组局部变量,这组局部变量就是这个方法在执行过程中所需的所有变量,包括一个指向this的引用、该方法的所有参数以及其他局部定义的变量。对于类方法(即static方法),其参数列表从0开始算起,而对于实例方法,位置0是用来存储this引用。

局部变量可以是如下形式:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference (引用)
  • returnAddress (返回地址)

除了long和double类型是两倍的长度(占64个bit,而不是其他类型的32个bit)占用两个连续的位置之外,所有变量在局部变量表中都只占有一个位置。

当一个变量被创建时,操作数栈就会被用来存储这个新的变量的值,然后这个值就会被存储到局部变量表中的正确位置。如果这个变量不是基本数据类型,那么局部变量中仅仅会存储它的一个引用,这个引用指向堆中对应存储的对象。

举个例子:

1
int i = 5;

被编译为:

1
2
0: bipush      5
2: istore_0
操作码说明
bipush 用于把一个byte作为一个int整型值放入操作数栈中,在这个例子中,5即被加入操作数栈中。
istore_0 形如istore_<n>的一组操作码中的一个,这组操作码用于把int整型值存储到局部变量中。<n>指示了局部变量表中的存储位置,取值只能是0、1、2或者3。另外一个用于处于位置大于3位的操作码是istore,这个操作码在使用的时候需要提供一个操作数用于表示需要存储到的局部变量表的位置。

当指令被执行的时候,内存里会发生这样的情况:

int i=69;对应的指令执行过程

类文件中同样为每一个方法包含了一个局部变量表,如果你的一个方法中包含了这样一句代码,那么你会在类文件的相应的方法的局部变量表中找到如下的条目:

局部变量表

Start(开始)Length (长度)Slot(位置个数)Name(名称)Signature(签名)
0 1 1 i I

 

成员变量(类变量)

成员变量(类变量)是作为类实例(对象)的一部分存储于堆之上的,其信息是被添加到类文件中的field_info的数组中的,就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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];
}

另外,如果成员变量是被初始化了的,那么初始化操作会被放到构造方法中执行。

当下面的Java代码被编译时:

1
2
3
public class SimpleClass {
    public int simpleField = 100;
}

使用javap工具查看字节码时,就会出现一个额外的片段,表明了成员变量被加入到了field_info数组中:

1
2
3
public int simpleField;
    Signature: I
    flags: ACC_PUBLIC

用来初始化的字节码是被加入到了构造方法中的,就像下面这样(用粗体标志):

1
2
3
4
5
6
7
8
9
10
11
public SimpleClass();
  Signature: ()V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=1, args_size=1
       0: aload_0
       1: invokespecial #1      //调用Object的init构造方法
       `4: aload_0
       5: bipush        100
       7: putfield      #2      //成员变量simpleField` 类型为int整型
      10: return
操作码说明
aload_0 从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶。尽管示例中的代码并不包含构造函数,但是类变量的初始化代码实际上会在由编译器创建的默认的构造函数中执行。因此,第一个局部变量实际上指向this,所以aload_0this装载到了操作数栈中。实际上,aload_0是一组格式为aload_<n>的操作码中的一个,这一组操作码把对象的引用装载到操作数栈中。<n>标志了待处理的局部变量表中的位置,但取值仅可为0、1、2或者3。还有一些其他相似的操作码用来装载非对象引用,包括iload_<n>lload_<n>fload_<n>dload_<n>,这里的i代表int型,l代表long型,f代表float型以及d代表double型。在局部变量表中的索引位置大于3的变量的装载可以使用iloadlloadfload,、dloadaload,这些操作码都需要一个操作数的参数,用于确认需要装载的局部变量的位置。
invokespecial 这指令用于调用实例的初始化方法,包括私有方法以及当前类的父类方法。它同样属于一组以不同方式来调用方法的操作码,这些操作码包括invokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtualinvokespecial是用于调用父类构造方法的指令,例如java.lang.Object的构造方法。
bipush 用于把一个int整型值放入操作数栈中,这个例子中是把100放入操作数栈中(原文此处误写为5,译者注)
putfield 从运行时的常量池中取一个指向成员变量的引用,这个成员变量的值以及其对应的对象都会从操作数栈中弹出,本例中的成员变量即为simpleField。例子中,aload_0首先向操作数栈中添加了对象,然后bipush向操作数栈中添加了100这个值,最后putfield从栈中弹出了这两个值,最终,这个对象的simpleField的值被设置为100

当指令被执行的时候,内存里会发生这样的情况:

代码执行时,内存变化情况

putfield只有一个指向常量池中的第二个位置的操作数。JVM为每一个类型都保持了常量池,尽管一个运行时数据结构包含了更多的数据,它还是非常类似一个符号表的结构。Java字节码需要运行时数据结构,但这些数据结构常常会比较大。如果直接放在字节码中,会占用太多的空间,所以Java把它存放在常量池中,而字节码中仅包含一个指向常量池的引用。当一个类文件被创建的时候,它拥有如下样式的常量池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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来标记的。

举个例子:

1
2
3
public class SimpleClass {
    public final int simpleField = 100;
}

成员变量的描述中增加了ACC_FINAL

1
2
3
4
public static final int simpleField = 100;
    Signature: I
    flags: ACC_PUBLIC, ACC_FINAL
    ConstantValue: int 100

然而,构造方法的初始化却没有被影响:

1
2
3
4: aload_0
5: bipush        100
7: putfield      #2 

 

静态变量

一个用static修饰的类的静态变量是在类文件中是以ACC_STATIC标记的:

1
2
3
public static int simpleField;
    Signature: I
    flags: ACC_PUBLIC, ACC_STATIC

用于初始化静态变量的字节码并不存在于初始化构造函数中,而是作为类构造函数的一部分被初始化的,而且初始化时使用的是putstatic操作符,而不是putfield操作符。

1
2
3
4
5
6
7
8
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-elseswitch语句,通过比较两个值的指令,控制字节码跳转到另一个分支上。

包括for循环while循环在内的循环语句的工作原理和条件选择类似,只不过他们通常还要包含一个goto指令,用来产生字节码的循环。do-while循环不需要任何的goto指令,这是由于它的条件分支判定就在字节码的尾部。更多的关于循环的内容,请阅读循环章节

一些操作码可以在一条指令中完成比较两个int整型值或者两个引用类型,并执行一个分支的操作,但是比较其他一些类型的数据如double、long、float时,是分两步的操作,首先比较操作被执行,然后把1、0或-1放到操作数栈顶,然后根据操作数栈中的值是大于、小于或者等于0来执行下一步的分支操作。
我们首先用一个例子解释if-else语句的编译执行过程,用于实现条件分支判定的不同指令将会在此之后做详细介绍。

 

if-else

接下来的代码示例展示了一个简单的用于比较两个int整型值的if-else语句。

1
2
3
4
5
6
7
public int greaterThen(int intOne, int intTwo) {
    if (intOne > intTwo) {
        return 0;
    } else {
        return 1;
    }
}

这个方法将会产生接下来的字节码:

1
2
3
4
5
6
7
0: iload_1
1: iload_2
2: if_icmple     7
5: iconst_0
6: ireturn
7: iconst_1
8: ireturn

首先两个参数使用iload_1iload_2指令载入到操作数栈中,然后if_icmple比较操作数栈顶的两个值,如果intOne小于等于intTwo,操作就会跳转到字节码的位置7所对应的分支。注意,这里的比较条件刚好和Java代码中的测试条件相反。因为,如果字节码中的测试条件成立,那么将会执行else部分的代码。对应的,如果Java代码中的测试条件成立,那么将会执行if部分的代码。换句话说,if_icmple是测试if条件是否为假并跳过if语句块(译者注:原文说的有点绕,其实就是在字节码中,默认顺序执行,只有条件不符合才跳转,所以才会出现判定条件刚好相反的情况,即判定的是不符合if语句的情况)。字节码中位置5和6的部分是if代码块,而7和8的部分是else代码块。

greaterThen

接下来的示例代码演示了一个稍微复杂一点的需要两步操作的比较的例子。

1
2
3
4
5
6
7
8
9
public int greaterThen(float floatOne, float floatTwo) {
    int result;
    if (floatOne > floatTwo) {
        result = 1;
    } else {
        result = 2;
    }
    return result;
}

这个方法产生了接下来的字节码:

1
2
3
4
5
6
7
8
9
10
11
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_1fload_2操作码将两个参数值放入操作数栈中,这个例子和之前的不同之处就是接下来的比较需要两步操作。第一步,先用fcmpl比较floatOnefloatTwo,然后把比较的结果按照如下的方式放入操作数栈中:

floatOne > floatTwo -> 1
floatOne = floatTwo -> 1
floatOne < floatTwo -> 1
floatOne or floatTwo = NaN -> 1

第二步,使用ifle判定前一步fcmpl的结果,如果是小于等于0,则跳转到位置11处的字节码所对应的分支。

这个示例和前一个的不同之处还在于它只有在代码的尾部才出现一个return语句,因此在if代码块的尾部需要使用一个goto语句来跳转,以防止继续执行else代码段。这个goto语句跳转到位置13的字节码处,该处的字节码使用iload_3指令把存储在局部变量表的第三个位置的变量放入操作数栈中,以便于接下来的return指令返回结果。

greaterThenFloat

除了比较数值的指令外,同样有比较引用的操作码,即==,以及与null比较的操作码,即== null!= null,还有用于判定对象类型的操作码,即instanceof

操作码说明
if_icmp<cond>
eq  
ne  
lt  
le  
gt  
ge  
这组操作码适用于比较操作数栈顶的两个int整型值,然后跳转到相应的字节码。其中<cond>可以是:
  eq – 等于
  ne – 不等于
  lt – 小于
  le – 小于等于
  gt – 大于
  ge – 大于等于
if_acmp<cond>
eq  
ne  
这两个操作码是用于测试两个引用是否相同(eq)或者不相同(ne),然后跳转到由操作数确定的对应的新的字节码的位置
ifnonnull
ifnull
这两个操作码是用来测试两个引用是否是null或者不是null,并跳转到由操作数确定的对应的新的字节码的位置
icmp 这个操作码是用来比较操作数栈中的两个int整型值,然后按照以下的规则向操作数栈中放入一个值:
  如果value1 > value2 -> 放入1
  如果value1 = value2 -> 放入0
  如果value1 < value2 -> 放入-1
fcomp<cond>
l  
g  
dcmp<cond>
l  
g  
这组操作码用于比较两个float或者double值,然后按照下面的规则,向操作数栈中放入一个值:
  如果value1 > value2 -> 放入1
  如果value1 = value2 -> 放入0
  如果value1 < value2 -> 放入-1
lg结尾的操作码的区别在于如何处理NaN,fcmpgdcmpg指令在遇到NaN时向操作数栈中放入int整型值1,而fcmpldcmpl指令在遇到NaN时向操作数栈中放入int整型值-1,后者保证了当待比较的数中的任何一个不是一个数字(NaN)时,比较都不会成功。举个例子,当测试是否x > y(x和y均为double类型)时,使用fcmpl使得当其中任何一个值为NaN时,操作数栈中都会被放入-1,而接下来的指令永远是ifle,用于判断值是否小于等于0。结果就是,只要x或者y中任何一个值为NaN,则ifle指令会使分支跳过if代码段,从而阻止if代码块的执行
instancof 这个操作码在操作数栈顶的对象是一个给定类的实例的时候,会向操作数栈中放入1,这个指令的操作数即使给定类在常量池中的索引位置。若果对象为null或者不是给定来的实例,则一个int整型值0将会被放入操作数栈中
if<cond>
eq  
ne  
lt  
le  
gt  
ge  
所有的这些操作码都是把操作数栈顶的值和0比较,并根据比较的结果将字节码跳转到给定的操作数所对应的位置。这些指令常常被用于实现复杂的条件逻辑,这些条件判定逻辑不能在一个指令中完成,例如测试一个方法的返回值。

 

switch

switch表达式中的类型必须为char、byte、short、int、Character、Byte、Short、Integer、String或enum类型。为了支持switch语句,JVM提供了两个特殊的指令——tableswitchlookupswitch,这两个指令都只能对int整型值进行操作,而char、byte、short和enmu类型都可以在内部转化为int类型,所以只能对int整型值操作不会是一个问题。Java 7中引入了对String类型的支持,这将在文章的后一部分做解释。

tableswitch通常是较快的操作码,但同样通常需要更多的内存。tableswitch的工作方式是列出位于最大和最小的case值之间所有潜在的case值,由于最大值和最小值是直接提供的。所以,一旦JVM发现switch的变量不在列出的case值的范围内,就会立即跳转到default代码块。那些Java代码中不包含的case值,只要位于最大值和最小值之间,都会被列出,只不过会指向default代码块。这样就能保证所有在位于最大值和最小值之间的case都有对应的结果。下面的例子演示了switch的代码:

1
2
3
4
5
6
7
8
9
10
11
12
public int simpleSwitch(int intOne) {
    switch (intOne) {
        case 0:
            return 3;
        case 1:
            return 2;
        case 4:
            return 1;
        default:
            return -1;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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这三个case值,每一个都对应了其预期的代码块,tableswitch同时还包含了2和3的case值,由于他们并未在Java代码中出现,所以指向默认的default代码块。当这个指令被执行的时候,会检查操作数栈顶的值是否在最大值和最小值之间,如果不在,则直接跳转到default代码块,也就是上面例子的位置42的字节码处。为了保证default代码块能够被找到,在tableswitch指令中,它总是出现在第一个字节(在字节码补齐之后)。如果操作数栈顶的值在最大值和最小值之间,则这个数就作为索引,在tableswitch中查找需要跳转的正确的字节码的位置,举个例子来说,上一个例子中,当操作数为1时,就会被跳转到位置38的字节码处。下面的图展示了这个字节码是如何执行的:

tableswitch

如果各个case值之间相隔比较远(即比较稀疏),这种做法就不可取了,因为这将消耗太多的内存。作为替代,当switch中的case比较稀疏时,就会采用lookupswitch指令。lookupswitch只列出每个case对应的字节码跳转,而非列出所有的可能值。当执行lookupswitch指令时,操作数栈顶的值会和lookupswitch中的每一个值进行比较,以决定跳转的地址。所以JVM执行该指令时,会在列表中搜索(查找)正确的匹配,因此lookupswitch指令是慢于tableswitch的,后者在执行时可以立即索引到对应的匹配。在编译switch语句时,编译器必须在内存消耗和性能之间做一个权衡,以决定使用哪一个指令。接下来的代码解释了lookupswitch的编译过程:

1
2
3
4
5
6
7
8
9
10
11
12
public int simpleSwitch(int intOne) {
    switch (intOne) {
        case 10:
            return 1;
        case 20:
            return 2;
        case 30:
            return 3;
        default:
            return -1;
    }
}

这将产生以下的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

 

String switch

Java 7中的switch语句增加了对String类型的支持。尽管现存的用于实现switch语句的操作码仅仅支持int整型值,但对String类型的支持并没有引入新的操作码,作为替代,String类型的switch语句的执行分为两步。首先会比较操作数栈顶的值和case语句的哈希码(hashcode),这个可以用lookupswitch或者tableswitch指令实现(取决于哈希码的稀疏程度),然后会跳转到调用精确匹配的String.equals()的字节码,最后对String.equals()的结果使用tableswitch指令,以跳转到正确的case分支。

1
2
3
4
5
6
7
8
9
10
11
12
public int simpleSwitch(String stringOne) {
    switch (stringOne) {
        case "a":
            return 0;
        case "b":
            return 2;
        case "c":
            return 3;
        default:
            return 4;
    }
}

String类型的switch将会产生以下的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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一文的运行时常量池章节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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指令和多个用于调用String.equal()的invokevirtual指令,关于invokevirtual指令的更多内容,请阅读下一篇文章的关于方法调用的章节。下图展示了对于输入“b”,字节码是如何执行的。

java_string_switch_byte_code_1

java_string_switch_byte_code_2

java_string_switch_byte_code_3

如果不同的case值对应的哈希码相同,如字符串”FB”和”Ea”的哈希码都是28,这种情况的处理方法是在执行equals方法时做一点小小的替换。请注意位置34处的字节码:ifeq 42,它跳转到另外一个对String.equals()的调用处,而非像之前的例子一样在不存在哈希码冲突的情况下使用lookupswitch操作码。

1
2
3
4
5
6
7
8
9
10
public int simpleSwitch(String stringOne) {
    switch (stringOne) {
        case "FB":
            return 0;
        case "Ea":
            return 2;
        default:
            return 4;
    }
}

这将生成如下的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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-elseswitch是通过使用操作码比较两个值,然后跳转到另外的字节码分支来实现的。更多的细节,请阅读条件选择这一章。

包括for循环while循环在内的循环采用的是类似的处理方式,不同之处在于他们通常还包含一个goto指令来产生字节码的循环,do-while循环不需要任何的goto指令,因为它的条件分支判定是放在字节码的最后。

一些操作码可以在同一条指令中比较两个int整型值或引用,并执行一个分支。而像double、long以及float类型的比较需要两步操作,首先是执行比较操作,把1、0或-1放入操作数栈中,然后根据操作数栈中的值是大于、小于或等于0来执行相应的分支。关于用于分支跳转的不同类型的指令的详细内容,请阅读本文前部的内容。

 

while循环

while循环由一个条件分支指令if_icmpgeif_icmplt(如前文所述)和一个goto语句组成。当条件不满足的时候,条件分支指令立即跳转到循环之后的指令,从而结束循环,循环的最后一句指令是goto,会把字节码的执行跳转到循环的开始部分,从而确保循环的执行,除非循环的条件不满足。其过程如下所示:

1
2
3
4
5
6
public void whileLoop() {
    int i = 0;
    while (i < 2) {
        i++;
    }
}

编译为:

1
2
3
4
5
6
7
8
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_icmpge指令测试位置1的局部变量(即i)是否大于等于2(此处原文误作10 译者注),如果是则跳到位置13(此处原文误作14 译者注)的字节码处,结束了循环。goto指令保持了字节码的循环执行,直到if_icmpge的条件被满足,这时就会立即执行到尾部的return指令。iinc是少见的直接更新一个局部变量而无需在操作数栈中进行读写的指令。在这个例子中,iinc指令给局部变量表中的第一个位置的值(即i)加1。

java_while_loop_byte_code_1

 

for循环

for循环while循环在字节码层面使用的相同的指令,这并不让人惊讶,因为所有的while循环都可以很容易的被重写为相同的for循环,前面提到的简单的while循环可以被重写为以下的for循环,他们产生的字节码是完全相同的。

1
2
3
4
5
public void forLoop() {
    for(int i = 0; i < 2; i++) {
        //do nothing
    }
}

 

do-while循环

do-while循环同样和for循环以及while循环很相似,除了前者无需goto指令,这是由于它的条件分支判定是最后一条指令,可以被用来跳转到循环的开始。

1
2
3
4
5
6
public void doWhileLoop() {
    int i = 0;
    do {
        i++;
    } while (i < 2);
}

产生如下的字节码:

1
2
3
4
5
6
7
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内部架构和字节码执行期间的使用的不同内存区域的更多细节,请阅读我之前的一篇深入JVM的文章。

原文链接: jamesdbloom 翻译: ImportNew.com xiafei
译文链接: http://www.importnew.com/13107.html

posted on 2017-02-21 16:53  让编程成为一种习惯  阅读(714)  评论(0编辑  收藏  举报