JVM字节码之字节码指令一

  • 字节码指令大致分类
  • 条件跳转类指令
  • 对象初始化指令

字节码指令的分类

javac 源码中 com.sun.tools.javac.jvm.ByteCodes 接口类, 列举了所有的字节码指令.

字节码根据用途不同可以有如下的分类:

  • 加载存储指令, 例如: iload, istore
  • 控制转移指令, 用于跳转, 例如: ifeq
  • 对象操作指令, 例如: new 用于创建实例
  • 方法调用指令, 例如: invokevirtual 用于调用对象的实例方法
  • 运算指令, 例如: iadd 加法
  • 线程同步指令, 例如: monitorenter, monitorexit
  • 异常处理指令, 例如: athrow 显式抛异常

条件跳转类型指令

ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、 if_icmpne、if_icmplt, if_icmpgt、if_icmple、if_icmpge、if_acmpeq、if_acmpne、tableswitch、lookupswitch、goto、goto_w、jsr、jsr_w、ret

循环遍历

1
2
3
4
5
6
7
8
public class Main {
private static int[] numbers = new int[]{1, 2, 3};
public static void main(String[] args) {
for (int number : numbers) {
System.out.println(number);
}
}
}

编译后关键的字节码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0: getstatic     #2                  // Field numbers:[I
3: astore_1
4: aload_1
5: arraylength
6: istore_2
7: iconst_0
8: istore_3
9: iload_3
10: iload_2
11: if_icmpge 33
14: aload_1
15: iload_3
16: iaload
17: istore 4
19: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
22: iload 4
24: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
27: iinc 3, 1
30: goto 9
33: return

其中循环体从 9: iload_333: return, 可以看到首先加载了 3位置上的索引值index 和 2位置上的数组长度length, 然后进行了比较 if_icmpge, 如果循环完成将会跳转到33, 如果未完成循环, 它会从1位置取数组array 和 3位置上的index, 用iaload指令把array中下标为index的元素, 加入到操作数栈中, 然后存储到4位置的局部变量表中, 之后调用打印相关的指令, 最后会对3位置上的index进行iinc操作(iinc操作不需要依赖栈进行,效率比较高), 并执行goto跳转到9.

for(item : array) 是Java为了方便开发搞的语法糖, 原形还是for(int i = 0;i < array.length; i++).

判断

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
int a = 1;
if (a == 1){
System.out.println(a);
}
}
}
1
2
3
4
5
6
7
8
9
0: iconst_1
1: istore_1
2: iload_1
3: iconst_1
4: if_icmpne 14
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

这里使用了 if_icmpne 指令.

switch-case

switch-case 有两种指令负责tableswitchlookupswitch, 两个的区别是:

  1. tableswitch, case必须是连续的, 针对case比较紧凑的情况下, 查找的效率比较高;
  2. lookupswitch, 查找方式上使用二分法, 应对与case比较稀疏的情况;

java在编译成字节码时, 会分析成本, 选择使用tableswitch, 还是使用lookupswitch, 下面举几个例子.

tableswitch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
int a = 1;
switch (a) {
case 1:
System.out.println("1");
break;
case 2:
System.out.println("2");
break;
case 4:
System.out.println("4");
break;
case 5:
System.out.println("5");
break;
default:
System.out.println("default");
}
}

生成如下:

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
0: iconst_1
1: istore_1
2: iload_1
3: tableswitch { // 1 to 5
1: 36
2: 47
3: 80
4: 58
5: 69
default: 80
}
36: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #3 // String 1
41: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
44: goto 88
47: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
50: ldc #5 // String 2
52: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
55: goto 88
58: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
61: ldc #6 // String 4
63: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: goto 88
69: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
72: ldc #7 // String 5
74: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
77: goto 88
80: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
83: ldc #8 // String default
85: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
88: return

可以看出switch-case中 case使用了 1,2,4,5;(没有使用3), 但是字节码中却自动补充了3, 那是应为java觉得补一个3并使用tableswitch的成本小于使用lookupswitch的成本, 所以字节码中自动补了case 3并让他跳转到default相同的位置.

lookupswitch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
int a = 1;
switch (a) {
case 100:
System.out.println("100");
break;
case 200:
System.out.println("200");
break;
case 400:
System.out.println("400");
break;
case 500:
System.out.println("500");
break;
default:
System.out.println("default");
}
}

生成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
0: iconst_1
1: istore_1
2: iload_1
3: lookupswitch { // 4
100: 44
200: 55
400: 66
500: 77
default: 88
}
44: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
47: ldc #3 // String 100
...略

这里使用了lookupswitch, 那是因为 100,200,400,500 跨度非常大, 如果自动补齐 100-200,200-400,400-500中的case, 并使用tableswitch, 这种的成本大于直接使用lookupswitch.

特殊情况

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
int a = 1;
switch (a) {
case 1:
System.out.println("1");
break;
case 2:
System.out.println("2");
break;
default:
System.out.println("default");
}
}

生成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0: iconst_1
1: istore_1
2: iload_1
3: lookupswitch { // 2
1: 28
2: 39
default: 50
}
28: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
31: ldc #3 // String 1
33: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: goto 58
39: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
42: ldc #5 // String 2
44: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
47: goto 58
50: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
53: ldc #6 // String default
55: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
58: return

这里case 只有 1和2, 而java选择了lookupswitch, 而不是tableswitch, 其实在case量比较小的情况下, 两种的效率没有大的差别, 之所以会选择lookupswitch, 要看看javac的源码(com.sun.tools.javac.jvm.Gen中的visitSwitch(JCSwitch tree)方法).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void visitSwitch(JCSwitch tree) {
// ...
long table_space_cost = 4 + ((long) hi - lo + 1); // words
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;
long lookup_time_cost = nlabels;
int opcode =
nlabels > 0 &&
table_space_cost + 3 * table_time_cost <=
lookup_space_cost + 3 * lookup_time_cost
?
tableswitch : lookupswitch;
// ...
}
  1. hi = 2 , lo = 1, nlabels = 2 的情况下, lookupswitch成本比较低
  2. hi = 3 , lo = 1, nlabels = 3 的情况下, tableswitch成本比较低

枚举类型的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
package com.yuda.test;

public class Main {
public static void main(String[] args) {
Type a = Type.A;
switch (a) {
case A:
System.out.println("A");
break;
case B:
System.out.println("B");
break;
case C:
System.out.println("C");
break;
default:
System.out.println("default");
}
}
}

enum Type {
A,
B,
C,
D,
}

生成如下:

1
2
3
4
5
6
7
8
9
10
11
4: getstatic     #3                  // Field com/yuda/test/Main$1.$SwitchMap$com$yuda$test$Type:[I
7: aload_1
8: invokevirtual #4 // Method com/yuda/test/Type.ordinal:()I
11: iaload
12: tableswitch { // 1 to 3
1: 40
2: 51
3: 62
default: 73
}
...略

可以看到使用了tableswitch, 对于枚举, 枚举类会有个ordinal方法, 每一个枚举项都会有个整数对应, 枚举类型被转换成整数类型来处理了.

字符串类型的switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) {
String a = args[0];
switch (a) {
case "1bcd":
System.out.println("1bcd");
break;
case "2111":
System.out.println("2111");
break;
case "33434":
System.out.println("33434");
break;
default:
System.out.println("default");
}
}
}

生成如下:

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
52
53
54
55
56
57
58
59
0: aload_0
1: iconst_0
2: aaload
3: astore_1
4: aload_1
5: astore_2
6: iconst_m1
7: istore_3
8: aload_2
9: invokevirtual #2 // Method java/lang/String.hashCode:()I
12: lookupswitch { // 3
1538207: 62
1557106: 48
48670517: 76
default: 87
}
48: aload_2
49: ldc #3 // String 1bcd
51: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
54: ifeq 87
57: iconst_0
58: istore_3
59: goto 87
62: aload_2
63: ldc #5 // String 2111
65: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
68: ifeq 87
71: iconst_1
72: istore_3
73: goto 87
76: aload_2
77: ldc #6 // String 33434
79: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
82: ifeq 87
85: iconst_2
86: istore_3
87: iload_3
88: tableswitch { // 0 to 2
0: 116
1: 127
2: 138
default: 149
}
116: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
119: ldc #3 // String 1bcd
121: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
124: goto 157
127: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
130: ldc #5 // String 2111
132: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
135: goto 157
138: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
141: ldc #6 // String 33434
143: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
146: goto 157
149: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
152: ldc #9 // String default
154: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
157: return

局部变量表为:

1
2
3
4
5
+───────+────+────────+─────────────+
| 0 | 1 | 2 | 3 |
+───────+────+────────+─────────────+
| args | a | tempa | matchIndex |
+───────+────+────────+─────────────+
  • args 是方法的参数
  • aString a = args[0]; 对应的
  • tempamatchIndex 后面看看.

通过字节码可以看出, 把入参 a 赋值给局部变量表下标为 2 的变量,记为tempa,初始化局部变量表 3 位置的变量为 -1,记为 matchIndex, 然后取字符串的hashCode, 使用lookupswitch 进行跳转, 分别跳转到62,48,76,87位置, 然后取出2位置保存的tempa, 使用String的equals()方法来判断是否相等, 如果不相等跳转到87位置, 如果相等, 会往3位置的matchIndex赋值, 可能赋的值为 0,1,2.

然后进入第二阶段, 从3位置取出刚刚赋的值, 通过tableswitch 来进行跳转, 分别对应116,127,138,149 各个位置, 执行各种不同的System.out.println()方法.

结论: 字符串使用switch-case, 就是通过字符串的hashCode, 进行一次跳转, 跳转后的逻辑是: 执行equals()方法, 如果符合条件, 则对一个标志位赋值, 然后再通过标志位的值来再进行一次跳转, 第二次跳转到真正需要执行的代码逻辑. (tableswitchlookupswitch的成本分析对这两次跳转依然是生效的)

hashCode相同的情况

如何造出hashCode相同的字符串? 答: 根据hashCode生成源码来看, 例如有个两个字符的字符串, 第一个字符-1, 第二个字符+31, 则这两个字符串hashCode相同.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) {
String a = args[0];
switch (a) {
case "CC":
System.out.println("1bcd");
break;
case "Bb":
System.out.println("2111");
break;
case "33434":
System.out.println("33434");
break;
default:
System.out.println("default");
}
}
}

得到如下:

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
52
53
54
55
56
57
58
0: aload_0
1: iconst_0
2: aaload
3: astore_1
4: aload_1
5: astore_2
6: iconst_m1
7: istore_3
8: aload_2
9: invokevirtual #2 // Method java/lang/String.hashCode:()I
12: lookupswitch { // 2
2144: 40
48670517: 68
default: 79
}
40: aload_2
41: ldc #3 // String Bb
43: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
46: ifeq 54
49: iconst_1
50: istore_3
51: goto 79
54: aload_2
55: ldc #5 // String CC
57: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
60: ifeq 79
63: iconst_0
64: istore_3
65: goto 79
68: aload_2
69: ldc #6 // String 33434
71: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
74: ifeq 79
77: iconst_2
78: istore_3
79: iload_3
80: tableswitch { // 0 to 2
0: 108
1: 119
2: 130
default: 141
}
108: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
111: ldc #8 // String 1bcd
113: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
116: goto 149
119: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
122: ldc #10 // String 2111
124: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
127: goto 149
130: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
133: ldc #6 // String 33434
135: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
138: goto 149
141: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
144: ldc #11 // String default
146: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
149: return

可以看到第一个lookupswitch 只有三种跳转逻辑, 那是BBAa的hashCode(2144)相同导致的. 2144 跳转到 40位置, 通过equals()方法判断是否等于Bb, 如果不等于再判断是否等于CC, 如果依然不等于将不会赋值.

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
String a = ""
int matchIndex = -1
switch(a.hashCode()) {
case 2144:
if (a.equals("Bb")){
matchIndex = 1
} else if (a.equals("CC")) {
matchIndex = 0
}
break;
case 48670517:
matchIndex = 2
default:
}
switch(matchIndex){
case 0:
System.out.println("1bcd");
case 1:
System.out.println("2111");
case 2:
System.out.println("33434");
default:
System.out.println("default");
}

对象初始化指令

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {

static int a = 1;

static {
a = 2;
}

public static void main(String[] args) {
Object o = new Object();
System.out.println(a);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
11: getstatic #4 // Field a:I
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return

static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #4 // Field a:I
4: iconst_2
5: putstatic #4 // Field a:I
8: return

main部分

new, dup, invokespecial 任何使用 new XXX() 来创建实例的方式, 都要这三个指令.

  1. 调用new, 创建一个实例, 但是实例的构造器没有执行.
  2. 调用dup, 操作数栈赋值一个栈顶元素.
  3. 调用invokespecial, 执行栈顶元素的构造器, 完成对象的创建.

为啥要dup? 答: 执行invokespecial指令时, 让操作数栈的栈顶元素出栈, 然后执行构造器, 然而invokespecial指令执行后, 不会再入栈(具体要看JVM源码), 最终导致与刚刚实例化的对象失去联系, astore_1 操作找不到对象, 所以需要曲线救国, invokespecial执行前先dup一下.

static 部分

<static> 是类的静态初始化 比 <init> 调用得更早一些,<static> 不会直接被调用,它在下面这个四个指令触发调用:new, getstatic, putstatic or invokestatic。也就是说,初始化一个类实例、访问一个静态变量或者一个静态方法,类的静态初始化方法就会被触发。

常见的题目

下面代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main {
public static void main(String[] args) {
A b = new B();
}
}

class A {
static {
System.out.println("A init");
}
public A() {
System.out.println("A Instance");
}
}

class B extends A {
static {
System.out.println("B init");
}
public B() {
System.out.println("B Instance");
}
}

结果:

1
2
3
4
A init
B init
A Instance
B Instance

先看下B的构造器的字节码

1
2
3
4
5
6
0: aload_0
1: invokespecial #1 // Method com/yuda/test/A."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String B Instance
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return

可以看到 如果要输出B Instance, 首先需要invokespecial一下A<init>

A,B实例化流程

  1. 首先需要执行B的构造器, 发现B的<static>没有执行, 于是需要先执行B的<static>.
  2. 发现A的<static> 也没有执行, 先执行A的<static>.
  3. A的<static>执行后, 开始执行B的<static>.
  4. B的<static>执行完成, 需要执行B的构造器
  5. 发现A的<static>没有执行.