# 字节码指令简单介绍

# i++ 与 ++i

下面来看看这两个操作的字节码是什么样子的

    public static void f3() {
        int i = 0;
        int j = i++;
        System.out.println(j);
    }
1
2
3
4
5
  public static void f3();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: iconst_0
         1: istore_0
         2: iload_0
         3: iinc          0, 1
         6: istore_1
         7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_1
        11: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 12: 0
        line 13: 2
        line 14: 7
        line 15: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2      13     0     i   I
            7       8     1     j   I
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    public static void f4() {
        int i = 0;
        int j = ++i;
        System.out.println(j);
    }
1
2
3
4
5
public static void f4();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: iconst_0
         1: istore_0
         2: iinc          0, 1
         5: iload_0
         6: istore_1
         7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_1
        11: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 18: 0
        line 19: 2
        line 20: 7
        line 21: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2      13     0     i   I
            7       8     1     j   I
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

精简下信息

i++ 对于如下指令

        int i = 0;
        int j = i++;

         0: iconst_0
         1: istore_0		 // 将数值 0 保存在本地变量 0 中
         2: iload_0			 // 将本地变量 0 压栈,也就是数值 0
         3: iinc          0, 1  // 将本地变量 0 + 1;这里操作的是本地变量,而不是操作数栈
         6: istore_1     // 将栈顶的结果(数值0)保存在本地变量 1 中
1
2
3
4
5
6
7
8

++i 对应如下指令

        int i = 0;
        int j = ++i;

         0: iconst_0
         1: istore_0		 // 将数值 0 保存在本地变量 0 中
         2: iinc          0, 1  // 将本地变量 0 + 1
         5: iload_0			 // 将本地变量 0 压栈,这里其实入栈的是相加之后的数值 1 了
         6: istore_1		 // 将栈顶结果(数值 1)保存在本地变量 1 中
1
2
3
4
5
6
7
8

这里的区别从指令上看出来有不同了,但是还是总结不出来区别是什么。在指令上的区别是:

  • i++:先将本地变量压栈,再对本地变量 + 1

    所以说这也是为什么在操作循环中对数组赋值时: arr[i++],i = 0, 时,操作的是 0,而不是 1 的原因

  • ++i:再对本地变量 + 1,再将本地变量压栈,

所以注意上面的结论和分析流程,因为这两个函数的结果也不一样:

int i = 0;
int j = i++;  // 打印 j 是 0

int i = 0;
int j = ++i;  // 打印 j 是 1
1
2
3
4
5

带着这个结果再去看指令,就会发现为什么会是 0 了;

  • int j = i++;

    2: iload_0			 // 将本地变量 0 压栈,也就是数值 0
    3: iinc          0, 1  // 将本地变量 0 + 1;这里操作的是本地变量,而不是操作数栈
    6: istore_1     // 将栈顶的结果(数值0)保存在本地变量 1 中
    
    1
    2
    3

    2 将数值 0 压栈、3 不是操作数栈,而是操作本地变量表、6 将栈顶结果也就是数值 0 保存到 j 中

  • int j = ++i

    2: iinc          0, 1  // 将本地变量 0 + 1
    5: iload_0			 // 将本地变量 0 压栈,这里其实入栈的是相加之后的数值 1 了
    6: istore_1		 // 将栈顶结果(数值 1)保存在本地变量 1 中
    
    1
    2
    3

    2 将本地变量0 + 1,再将本地变量 0 压栈(这是入栈的数值是 1),将栈的结果 1 保存到 j 中

再来看看下面的例子,比较两种写法那种性能高

    public static void fun1(){
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
1
2
3
4
5
  public static void fun1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: iconst_0
         1: istore_0
         2: iload_0							// 将变量i,也就是本地变量 0 压栈(第一次数值 0)
         3: bipush        10		// 把 10 压栈
         5: if_icmpge     21		// 如果 1 >= 2,也就是栈里的两个元素,i >= 10 ,就执行 21 这个指令,也就是 return 
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_0						 // 本地变量 0 压栈
        12: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        15: iinc          0, 1  // 将本地变量 0 + 1
        18: goto          2		  // 跳转到第 2 条指令,继续执行
        21: return
      LineNumberTable:
        line 24: 0
        line 25: 8
        line 24: 15
        line 27: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2      19     0     i   I
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 2
          locals = [ int ]
        frame_type = 250 /* chop */
          offset_delta = 18
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
    public static void fun2(){
        for (int i = 0; i < 10; ++i) {
            System.out.println(i);
        }
    }
1
2
3
4
5
  public static void fun2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: iconst_0
         1: istore_0
         2: iload_0
         3: bipush        10
         5: if_icmpge     21
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_0
        12: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        15: iinc          0, 1
        18: goto          2
        21: return
      LineNumberTable:
        line 29: 0
        line 30: 8
        line 29: 15
        line 32: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2      19     0     i   I
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 2
          locals = [ int ]
        frame_type = 250 /* chop */
          offset_delta = 18
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

对于两个方法的输出结果都是 0~9,可以发现两种写法生成的指令都是一样的,他们两个的写法会被优化为一样的,所以他们两种写法性能是一样的。

那么这里的 ++ii++ 被优化成什么了呢?这里是先对本地变量压栈,再对本地变量增加 1,所以是都被优化成了 ++i

# 字符串 + 拼接原理

在循环中做字符串 + 操作

    public static void fun5() {
        String str = "";
        for (int i = 0; i < 10; ++i) {
            str += i;
        }
        System.out.println(str);
    }
1
2
3
4
5
6
7

用 idea 直接打开 class 文件看到的内容是

    public static void fun5() {
        String str = "";

        for(int i = 0; i < 10; ++i) {
            str = str + i;
        }

        System.out.println(str);
    }
1
2
3
4
5
6
7
8
9

字节码的内容为

public static void fun5();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         // 常量压栈,也就是空字符串
         0: ldc           #6                  // String
         2: astore_0
         3: iconst_0
         4: istore_1
         5: iload_1
         6: bipush        10
         8: if_icmpge     36
        11: new           #7                  // class java/lang/StringBuilder
        14: dup
        15: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
        18: aload_0
        19: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: iload_1
        23: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        26: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        29: astore_0
        30: iinc          1, 1
        33: goto          5
        36: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        39: aload_0
        40: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        43: return
      LineNumberTable:
        line 37: 0
        line 38: 3
        line 39: 11
        line 38: 30
        line 41: 36
        line 42: 43
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            5      31     1     i   I
            3      41     0   str   Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 5
          locals = [ class java/lang/String, int ]
        frame_type = 250 /* chop */
          offset_delta = 30
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

从字节码中可以看到,它在每一次循环中都创建了一个 StringBuilder 来使用 append 方法来相加,最后使用 toString 来得到相加的结果。

# Try-Finally

看如下一段代码,它的返回值是什么?

    public static String fun6() {
        String str = "hello";
        try {
            return str;
        } finally {
            str = "mrcode";
        }
    }
1
2
3
4
5
6
7
8

字节码内容

public static java.lang.String fun6();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=0
         // 将常量池中的 #13 常量压栈
         0: ldc           #13                 // String hello
         2: astore_0  // 存储到本地变量表 0
         3: aload_0   // 本地变量表 0 压栈
         4: astore_1	// 将结果存储到本地变量表 1
         // 这里就是执行 finall 里面的代码了,不过是将 #14 常量压入栈
         5: ldc           #14                 // String mrcode
         7: astore_0  // 然后将结果保存到了本地变量表 0 中,也就是 str = mrcode 这个结果了
         8: aload_1  // 加载本地变量表 1,也就是 hello
         9: areturn  // 然后返回了 hello
        // 后面的代码是因为 try 的原因,如果说发生异常的话,则会走下面这段代码
        10: astore_2 // 把异常存储到本地变量 2 中
        11: ldc           #14                 // String mrcode
        13: astore_0 // 将 mrcode 存储在本地变量 1 中
        14: aload_2 // 加载异常
        15: athrow // 然后抛出去
      Exception table:
         from    to  target type
             3     5    10   any
      LineNumberTable:
        line 46: 0
        line 48: 3
        line 50: 5
        line 48: 8
        line 50: 10
        line 51: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3      13     0   str   Ljava/lang/String;
      StackMapTable: number_of_entries = 1
        frame_type = 255 /* full_frame */
          offset_delta = 10
          locals = [ class java/lang/String ]
          stack = [ class java/lang/Throwable ]
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

从字节码看,返回的是变量表 1 的值,但是 finall 里面的字符串相加操作是对变量表 0 相加的。还是先执行的。

所以:从字节码来看,无论是否是异常都会走 finally 的代码。

# String Constant Variable

前面看到的 + 拼接字符串,字节码使用了 StringBuild 来操作,那么是否是所有的 + 操作都会使用 StringBuild 呢?

实际上不是的,这个结论就与本次的知识点 字符串常量变量 有关

看下面的两段代码

    public static void fun7() {
        final String x = "hello";
        final String y = x + "world";
        String z = x + y;
        System.out.println(z);
    }

    public static void fun8() {
        final String x = "hello";
        String y = x + "world"; // 唯一的区别是这句代码没有 final
        String z = x + y;
        System.out.println(z);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13

这样看感觉是看不出什么问题,fun7 的字节码指令

         0: ldc           #12                 // String hello
         2: astore_0
         // 这里直接将 x + "world" 变成了常量池了,直接压栈
         3: ldc           #14                 // String helloworld
         5: astore_1
         // 同理,这里直接样 x + y 变成了常量池中的常量,直接压栈了
         6: ldc           #15                 // String hellohelloworld
         8: astore_2
         // 后面就是打印结果了
         9: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        12: aload_2
        13: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: return
1
2
3
4
5
6
7
8
9
10
11
12
13

fun8 的字节码指令

         0: ldc           #12                 // String hello
         2: astore_0
         // 这里直接将 x + "world" 变成了常量池了,直接压栈  
         3: ldc           #14                 // String helloworld
         5: astore_1
         // 从 x+y 开始,就不一样了,new 了 一个 StringBuilder
           // 从知识点来看:y 不是一个常量了,是一个变量(因为没有 final)
           // x 是常量 + y 变量,变成另外一个变量 z
           // 可以看到下面的操作则是,初始化一个 StringBuilder,然后 append("hello").append("helloworld")
         6: new           #7                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
        13: ldc           #12                 // String hello
        15: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        18: aload_1
        19: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        25: astore_2
        26: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        29: aload_2
        30: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        33: return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这就是今天的知识点 JAVA 语言规范中的 Final Variables (opens new window)

A constant variable is a final variable of primitive type or type String that is initialized with a constant expression (§15.28 (opens new window)).

常量变量是 常量表达式 (opens new window) 初始化的原始类型或则 final String 类型的

在常量表达式中,关注下 String 相关的,有一个概念是 String Literals (opens new window),也就是 字符串字面量

然后下面给了一个例子

package testPackage;
class Test {
    public static void main(String[] args) {
        String hello = "Hello", lo = "lo";
        // 相等:因为 hello 变量引用的是常量池里面的, “Hello” 是字面常量,它存储在常量池里面的
        System.out.print((hello == "Hello") + " ");
        // 相等:也是常量池中的
        System.out.print((Other.hello == hello) + " ");
        // 相等:两个字面常量相加,在编译时替换成字面常量了
        System.out.print((hello == ("Hel"+"lo")) + " ");
        // 不相等: 这里一个字面量 + 一个变量 lo(因为 lo 不是 final 的),无法发生编译替换了
        System.out.print((hello == ("Hel"+lo)) + " ");
        // 相等:使用了 intern,访问的是常量池里面的字符串
          // 简单说:常量池里面没有该字符串,则加入该字符串,有则直接使用
          // 但是 jdk 版本不同,它的内存结构也不相同,可能导致有些结果不同版本不相同
        System.out.println(hello == ("Hel"+lo).intern());
    }
}
class Other { static String hello = "Hello"; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19