- 字节码指令大致分类
- 条件跳转类指令
- 对象初始化指令
字节码指令的分类
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_3
到 33: 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
有两种指令负责tableswitch
和lookupswitch
, 两个的区别是:
tableswitch
, case必须是连续的, 针对case比较紧凑的情况下, 查找的效率比较高;
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); long table_time_cost = 3; 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; }
|
hi
= 2 , lo
= 1, nlabels
= 2 的情况下, lookupswitch
成本比较低
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
是方法的参数
a
是 String a = args[0];
对应的
tempa
和matchIndex
后面看看.
通过字节码可以看出, 把入参 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()
方法, 如果符合条件, 则对一个标志位赋值, 然后再通过标志位的值来再进行一次跳转, 第二次跳转到真正需要执行的代码逻辑. (tableswitch
与lookupswitch
的成本分析对这两次跳转依然是生效的)
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
只有三种跳转逻辑, 那是BB
和 Aa
的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()
来创建实例的方式, 都要这三个指令.
- 调用new, 创建一个实例, 但是实例的构造器没有执行.
- 调用dup, 操作数栈赋值一个栈顶元素.
- 调用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实例化流程
- 首先需要执行B的构造器, 发现B的
<static>
没有执行, 于是需要先执行B的<static>
.
- 发现A的
<static>
也没有执行, 先执行A的<static>
.
- A的
<static>
执行后, 开始执行B的<static>
.
- B的
<static>
执行完成, 需要执行B的构造器
- 发现A的
<static>
没有执行.