# 字节码指令简单介绍
# i++ 与 ++i
下面来看看这两个操作的字节码是什么样子的
public static void f3() {
int i = 0;
int j = i++;
System.out.println(j);
}
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
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);
}
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
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 中
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 中
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
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
32 将数值 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
32 将本地变量0 + 1,再将本地变量 0 压栈(这是入栈的数值是 1),将栈的结果 1 保存到 j 中
再来看看下面的例子,比较两种写法那种性能高
public static void fun1(){
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
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
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);
}
}
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
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,可以发现两种写法生成的指令都是一样的,他们两个的写法会被优化为一样的,所以他们两种写法性能是一样的。
那么这里的 ++i
和 i++
被优化成什么了呢?这里是先对本地变量压栈,再对本地变量增加 1,所以是都被优化成了 ++i
# 字符串 +
拼接原理
在循环中做字符串 + 操作
public static void fun5() {
String str = "";
for (int i = 0; i < 10; ++i) {
str += i;
}
System.out.println(str);
}
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);
}
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
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";
}
}
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 ]
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);
}
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
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
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 typeString
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"; }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19