美文网首页
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;
    

    相关文章

      网友评论

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

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