美文网首页
03 Java字节码技术

03 Java字节码技术

作者: 攻城老狮 | 来源:发表于2021-09-21 14:45 被阅读0次

1 字节码角度分析 a++

  1. 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
    }
}
  1. 反编译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
  1. 分析
  • iinc指令是直接在局部变量槽位slot上进行运算
  • a++ 和 ++a 的区别是先执行 iload 还是先执行 iinc
    • a++是先加载iload,再自增iinc
    • ++a是先自增iinc,再加载iload

2 条件判断指令

  1. Java条件判断的字节码
image-20210921091934577.png
  • byte,short,char 都会按int比较,因为操作数栈都是4字节
  1. Java代码
// 从字节码角度来分析:条件判断指令
public class TestDemo {
    public static void main(String[] args) {
        int a = 0;
        if (a == 0) {
            a = 10;
        } else {
            a = 20;
        }
    }
}
  1. 反编译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循环

  1. Java代码
// 从字节码角度来分析:循环控制指令
public class TestDemo {
    public static void main(String[] args) {
        int a = 0;
        while (a < 10) {
            a++;
        }
    }
}
  1. 反编译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循环

  1. Java代码
// 从字节码角度来分析:循环控制do while指令
public class TestDemo {
    public static void main(String[] args) {
        int a = 0;
        do {
            a++;
        } while (a < 10);
    }
}
  1. 反编译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循环

  1. Java代码
// 从字节码角度来分析:循环控制 for 指令
public class TestDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            
        }
    }
}
  1. 反编译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 分析循环的判断结果

  1. 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
    }
}
  1. 反编译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

  1. 分析
  • x = x++ 操作是先从局部变量表中取数据到栈顶
  • 然后局部变量表数据自增1
  • 最后从栈顶弹数据出来存入局部变量表中

所以:x = x++,最终结果还是和之前的初始数据值是致。

4 构造方法

4.1 <cinit>()V 方法

  1. Java代码
// 从字节码角度来分析:构造方法
public class TestDemo {
    static int i = 10;
 
    static {
        i = 20;
    }
 
    static {
        i = 30;
    }
}
  1. 反编译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 方法

  1. 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
    }
}
  1. 反编译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 方法调用

  1. 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
    }
}
  1. 反编译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是一种动态分派的调用指令:也就是引用的类型并不能决定方法属于哪个类型。

  1. 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("吃鱼");
    }
}
  1. 反编译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
  1. 分析

    通过对象找到它的 Class 类,获取到它的虚方法表后,就能确定虚方法表中每个方法实际的方法入口地址。有的来自于自己(eat方法),有的来自于父类(toString方法)。将来,对象调用方法时,就能明确知道调用哪个方法了。虚方法表是在类的加载过程的链接阶段生成的,所以在链接阶段就已经确定了虚方法表每个方法的入口地址

  • invokevirtual 指令调用的对象vtable中的方法
  • 多态方法调用,当执行 invokevirtual指令时:
    • 先通过栈帧中的对象引用找到对象
    • 分析对象头,找到对象的实际Class
    • Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
    • 查表到得方法的具体地址
    • 执行方法的字节码
  • 多态方法调用,需要在执行过程中经过虚方法表多次查找,过程比较复杂。如果从细微的效率来说,它是不如static。jvm底层也做很多虚方法表查找过程的优化,比如缓存、经常查找的方法放入缓存,这样查找较快。如果Animal只有Dog继承,没有Cat继承的话,jvm会将多态转换为单态,这样加快方法的寻址速度。

7 异常处理

7.1 catch

  1. Java代码
// 从字节码角度来分析:异常处理
public class TestDemo {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        }
    }
}
  1. 反编译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

  1. 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;
        }
    }
}
  1. 反编译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

  1. 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");
    }
}
  1. 反编译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

  1. 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;
        }
    }
}
  1. 反编译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

  1. 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;
        }
    }
}
  1. 反编译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
  1. 举例吞掉异常
// 从字节码角度分析: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 对返回值的影响

  1. 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;
        }
    }
}
  1. 反编译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

  1. Java代码
// 从字节码角度分析:synchronized
public class TestDemo {
    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}
  1. 反编译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;

相关文章

  • 字节码注入

    Java 动态字节码技术

  • 03 Java字节码技术

    1 字节码角度分析 a++ Java代码 反编译Java代码 分析 iinc指令是直接在局部变量槽位slot上进行...

  • 字节码引用检测原理与实战

    一、字节码与引用检测 1.1 Java字节码 本章中的字节码重点研究Java 字节码,Java字节码(Java b...

  • 字节码技术

    字节码技术应用场景 AOP技术、Lombok去除重复代码插件、动态修改class文件等 字节技术优势 Java字节...

  • 程序员练级攻略(2018):Java底层知识

    Java 字节码相关 首先,Java 最黑科技的玩法就是字节码编程,也就是动态修改或是动态生成 Java 字节码。...

  • Java并发机制的底层原理

    Java程序执行:Java代码→Java字节码→字节码被类加载器加载到JVM里,JVM执行字节码→转化为汇编指令在...

  • 奇门遁甲之字节码与JVM指令

    最近在研究ASM 字节码增强技术,要掌握ASM 必须要先连接Java字节码结构、JVM栈帧和常用JVM指令。 本章...

  • DVM执行 java 程序的工具

    jvm 执行字节码原理:java 程序运行时,是由一个 java 虚拟机来解释 java 字节码的,它将这些字节码...

  • Java字节码

    参考链接:一文让你明白Java字节码 Java字节码 Java虚拟机字节码指令 Java号称是一门“一次编译到处运...

  • java反射

    /** *Demo描述: *Android中Java反射技术的使用示例 *在Java中描述字节码文件(xxx.cl...

网友评论

      本文标题:03 Java字节码技术

      本文链接:https://www.haomeiwen.com/subject/fcnbgltx.html