1 字节码角度分析 a++
- Java代码
public class TestDemo {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--; // 10 + 12 + 12
System.out.println(a); //11
System.out.println(b); //34
}
}
- 反编译Java代码
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
21: iload_1
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
- 分析
- iinc指令是直接在局部变量槽位slot上进行运算
- a++ 和 ++a 的区别是先执行 iload 还是先执行 iinc
- a++是先加载iload,再自增iinc
- ++a是先自增iinc,再加载iload
2 条件判断指令
- Java条件判断的字节码
- byte,short,char 都会按int比较,因为操作数栈都是4字节
- Java代码
// 从字节码角度来分析:条件判断指令
public class TestDemo {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}
- 反编译Java代码
0: iconst_0
1: istore_1
2: iload_1
3: ifne 12
6: bipush 10
8: istore_1
9: goto 15
12: bipush 20
14: istore_1
15: return
3 循环控制指令
3.1 while循环
- Java代码
// 从字节码角度来分析:循环控制指令
public class TestDemo {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
- 反编译Java代码
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
3.2 do while循环
- Java代码
// 从字节码角度来分析:循环控制do while指令
public class TestDemo {
public static void main(String[] args) {
int a = 0;
do {
a++;
} while (a < 10);
}
}
- 反编译Java代码
0: iconst_0
1: istore_1
2: iinc 1, 1 //编译器做了优化,a++ 先在slot槽上自增再获取。如果再定义变量 b = a++ + ++a,则又会变成正常的字节码加载流程
5: iload_1
6: bipush 10
8: if_icmplt 2
11: return
3.3 for循环
- Java代码
// 从字节码角度来分析:循环控制 for 指令
public class TestDemo {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
}
}
}
- 反编译Java代码
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
- 比较while 和 for 的字节码,会发现它们是一模一样的。所以我们编写的while 循环 与 for循环在底层是一样的执行方式。
3.4 分析循环的判断结果
- Java代码
// 从字节码角度来分析:判断结果
public class TestDemo {
public static void main(String[] args) {
int i = 0 ;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x); // 结果是0
}
}
- 反编译Java代码
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1
5: bipush 10
7: if_icmpge 21
10: iload_2
11: iinc 2, 1
14: istore_2 //前面3行表示先加载x到栈,然后slot槽位+1,然后再将栈数据覆盖slot槽位的数据。最后的结果是slot槽位的数据未发生改变
15: iinc 1, 1
18: goto 4
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_2
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
- 分析
- x = x++ 操作是先从局部变量表中取数据到栈顶
- 然后局部变量表数据自增1
- 最后从栈顶弹数据出来存入局部变量表中
所以:x = x++,最终结果还是和之前的初始数据值是致。
4 构造方法
4.1 <cinit>()V 方法
- Java代码
// 从字节码角度来分析:构造方法
public class TestDemo {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}
- 反编译Java代码
- 编译器会按从上至下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为一个特殊的方法<cinit>()V
- <cinit>()V 方法会在类加载的初始化阶段被调用
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #2 // Field i:I
5: bipush 20
7: putstatic #2 // Field i:I
10: bipush 30
12: putstatic #2 // Field i:I
15: return
LineNumberTable:
line 6: 0
line 9: 5
line 13: 10
line 14: 15
4.2 Init 方法
- Java代码
// 从字节码角度来分析:构造方法
public class TestDemo {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public TestDemo(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
TestDemo d = new TestDemo("s3", 30);
System.out.println(d.a); // "s3"
System.out.println(d.b); // 30
}
}
- 反编译Java代码
- 编译器会按从上至下的顺序,收集所有{ } 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后
public com.yqj.TestDemo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
25: putfield #3 // Field a:Ljava/lang/String;
28: aload_0 // -----------------------------------构造方法-----
29: aload_1 // <- slot 1(a) "s3" |
30: putfield #3 // -> this.a |
33: aload_0 |
34: iload_2 // <- slot 2(b) 30 |
35: putfield #4 // -> this.b ----------------------
38: return
LineNumberTable:
line 17: 0
line 6: 4
line 8: 10
line 11: 16
line 14: 22
line 18: 28
line 19: 33
line 20: 38
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 this Lcom/yqj/TestDemo;
0 39 1 a Ljava/lang/String;
0 39 2 b I
5 方法调用
- Java代码
// 从字节码角度来分析:方法调用
public class TestDemo {
// 构造方法
public TestDemo() {}
// 私有成员方法 test1
private void test1() {}
// 私有最终方法 test2
private final void test2() {}
// 公开成员方法 test3
public void test3() {}
// 公开静态方法 test4
public static void test4() {}
public static void main(String[] args) {
TestDemo d = new TestDemo();
d.test1(); // 通过对象.调用私有成员方法 test1
d.test2(); // 通过对象.调用私有最终方法 test2
d.test3(); // 通过对象.调用公开成员方法 test3
d.test4(); // 通过对象.调用公开静态方法 test4
TestDemo.test4(); // 通过类.调用私有成员方法 test4
}
}
- 反编译Java代码
- 通过实例对象.调用静态方法,在字节码层面角度可以看到入栈又出栈,效率较低
- 说明:
- invokespecial 与 invokestatic 两者性能差不多
- invokevirtual 则需要找几次才能确定方法
0: new #2 // class com/yqj/TestDemo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V
25: invokestatic #7 // Method test4:()V
28: return
6 多态原理
-
invokespecial只能调用三类方法、<init>方法、private方法、super.method()。因为这三类方法的调用对象在编译时就可以确定。
-
invokevirtual是一种动态分派的调用指令:也就是引用的类型并不能决定方法属于哪个类型。
- Java代码
// 从字节码角度来分析:多态原理
/**
* 演示多态原理,注意加上下面的 JVM参数,禁用指针压缩
* -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
*/
public class TestDemo {
// 此处,就会有多态应用。
public static void test(Animal animal) {
// 因为animal有可能是狗,也可能是猫,且不同对象eat方法实现不一样
// 后续将演示eat方法是哪个对象调用,从字节码角度对eat方法查找调用过程
animal.eat();
System.out.println(animal.toString());
}
public static void main(String[] args) throws IOException {
test(new Cat());
test(new Dog());
System.in.read();
}
}
abstract class Animal {
public abstract void eat();
@Override
public String toString() {
return "我是" + this.getClass().getSimpleName();
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("啃骨头");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("吃鱼");
}
}
- 反编译Java代码
- 因为Animal具体实例有可能是狗,也可能是猫,且不同对象 eat() 方法实现不一样
0: aload_0
1: invokevirtual #2 // Method com/yqj/Animal.eat:()V
4: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
7: aload_0
8: invokevirtual #4 // Method com/yqj/Animal.toString:()Ljava/lang/String;
11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: return
-
分析
通过对象找到它的 Class 类,获取到它的虚方法表后,就能确定虚方法表中每个方法实际的方法入口地址。有的来自于自己(eat方法),有的来自于父类(toString方法)。将来,对象调用方法时,就能明确知道调用哪个方法了。虚方法表是在类的加载过程的链接阶段生成的,所以在链接阶段就已经确定了虚方法表每个方法的入口地址
- invokevirtual 指令调用的对象vtable中的方法
- 多态方法调用,当执行 invokevirtual指令时:
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际Class
- Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表到得方法的具体地址
- 执行方法的字节码
- 多态方法调用,需要在执行过程中经过虚方法表多次查找,过程比较复杂。如果从细微的效率来说,它是不如static。jvm底层也做很多虚方法表查找过程的优化,比如缓存、经常查找的方法放入缓存,这样查找较快。如果Animal只有Dog继承,没有Cat继承的话,jvm会将多态转换为单态,这样加快方法的寻址速度。
7 异常处理
7.1 catch
- Java代码
// 从字节码角度来分析:异常处理
public class TestDemo {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
}
- 反编译Java代码
- 可以看到多出来一个Exception table的结构,[from, to)是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过type匹配异常类型,如果一致,进入target所指示行号
- 8行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2号位置
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2 // 将异常对象引用存入局部变量表的 slot 2号位置
9: bipush 20
11: istore_1
12: return
Exception table:
from to target type
2 5 8 Class java/lang/Exception
LineNumberTable:
line 8: 0
line 10: 2
line 13: 5
line 11: 8
line 12: 9
line 14: 12
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/Exception;
0 13 0 args [Ljava/lang/String;
2 11 1 i I
7.2 多个catch
- Java代码
// 从字节码角度来分析:多个 single-catch 块的情况
public class TestDemo {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (ArithmeticException e) {
i = 30;
} catch (NullPointerException e) {
i = 40;
} catch (Exception e) {
i = 50;
}
}
}
- 反编译Java代码
- 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用。也算是一种优化,为节省栈帧内存的使用
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 26
8: astore_2
9: bipush 30
11: istore_1
12: goto 26
15: astore_2
16: bipush 40
18: istore_1
19: goto 26
22: astore_2
23: bipush 50
25: istore_1
26: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/NullPointerException
2 5 22 Class java/lang/Exception
LineNumberTable:
line 8: 0
line 10: 2
line 17: 5
line 11: 8
line 12: 9
line 17: 12
line 13: 15
line 14: 16
line 17: 19
line 15: 22
line 16: 23
line 18: 26
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/ArithmeticException;
16 3 2 e Ljava/lang/NullPointerException;
23 3 2 e Ljava/lang/Exception;
0 27 0 args [Ljava/lang/String;
2 25 1 i I
7.3 multi-catch
- Java代码
// 从字节码角度来分析:multi-catch 的情况
// jdk1.7 新增multi catch
public class TestDemo {
public static void main(String[] args) {
try {
Method test = TestDemo.class.getMethod("test");
test.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
public static void test() {
System.out.println("ok");
}
}
- 反编译Java代码
- 异常-multi_catch 字节码 对比 异常-多个catch字节码,并没有特别的地方,只是异常表Exception table 三个异常的 target都一致。也有 异常-多个catch字节码 异常对象引用在局部变量表中槽位复用。
0: ldc #2 // class com/yqj/TestDemo
2: ldc #3 // String test
4: iconst_0
5: anewarray #4 // class java/lang/Class
8: invokevirtual #5 // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
11: astore_1
12: aload_1
13: aconst_null
14: iconst_0
15: anewarray #6 // class java/lang/Object
18: invokevirtual #7 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
21: pop
22: goto 30
25: astore_1
26: aload_1
27: invokevirtual #11 // Method java/lang/ReflectiveOperationException.printStackTrace:()V
30: return
Exception table:
from to target type
0 22 25 Class java/lang/NoSuchMethodException
0 22 25 Class java/lang/IllegalAccessException
0 22 25 Class java/lang/reflect/InvocationTargetException
LineNumberTable:
line 10: 0
line 11: 12
line 14: 22
line 12: 25
line 13: 26
line 15: 30
LocalVariableTable:
Start Length Slot Name Signature
12 10 1 test Ljava/lang/reflect/Method;
26 4 1 e Ljava/lang/ReflectiveOperationException;
0 31 0 args [Ljava/lang/String;
7.4 finally
- Java代码
// 从字节码角度分析:异常_finally
public class TestDemo {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e){
i = 20;
} finally {
i = 30;
}
}
}
- 反编译Java代码
- 可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程, catch 流程以入 catch 剩余的异常类型流程
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: bipush 30
7: istore_1
8: goto 27
11: astore_2
12: bipush 20
14: istore_1
15: bipush 30
17: istore_1
18: goto 27
21: astore_3 // catch any -> slot 3 其他的异常
22: bipush 30
24: istore_1
25: aload_3
26: athrow
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
7.5 finally中出现return
- Java代码
// 从字节码角度分析:finally 出现了 return
public class TestDemo {
public static void main(String[] args) {
int result = test();
System.out.println(result); //20
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
- 反编译Java代码
- 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 为准
- 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常。
0: bipush 10
2: istore_0 // 10 -> slot 0 (从栈顶移除了) 用于固定返回结果
3: bipush 20
5: ireturn
6: astore_1
7: bipush 20
9: ireturn
Exception table:
from to target type
0 3 6 any
- 举例吞掉异常
// 从字节码角度分析:finally 出现了 return
public class TestDemo {
public static void main(String[] args) {
int result = test();
System.out.println(result); //20
}
public static int test() {
try {
int i = 1 / 0;
return 10;
} finally {
return 20;
}
}
}
7.6 finally 对返回值的影响
- Java代码
// 从字节码角度分析:finally 出现了 return
public class TestDemo {
public static void main(String[] args) {
int result = test();
System.out.println(result); //10
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
- 反编译Java代码
- return i; 对应的字节码为 istore_1 目的是将 10 存入局部变量表1号槽位,目的是为了固定返回值
- 只要不在 finally 代码块中 return ,其异常是不会被吞掉
0: bipush 10
2: istore_0
3: iload_0
4: istore_1 // 10 -> slot 1, 暂存至 slot 1, 目的是为了固定返回值
5: bipush 20
7: istore_0
8: iload_1 // <- slot 1(10) 载入 slot 1 暂存的值
9: ireturn
10: astore_2
11: bipush 20
13: istore_0
14: aload_2
15: athrow
Exception table:
from to target type
3 5 10 any
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I
8 Synchronized
- Java代码
// 从字节码角度分析:synchronized
public class TestDemo {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
- 反编译Java代码
- synchronized 是通过 monitorenter和 monitorexit 实现锁的获取和释放,并且他俩是成对出现。 monitorenter 入口只有一个,但是 monitorexit 的出口有多个,因为程序异常也需要将锁释放
- 当代码段执行结束或出现异常后会自动释放对监视器的锁定。
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: aload_1
9: dup
10: astore_2 // lock引用 -> slot 2
11: monitorenter // monitorenter (lock引用)
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #4 // String ok
17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: aload_2 // <- slot 2 (lock引用)
21: monitorexit // monitorexit (lock引用)
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // <- slot 2 (lock引用)
27: monitorexit // monitorexit (lock引用)
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable:
line 9: 0
line 10: 8
line 11: 12
line 12: 20
line 13: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
网友评论