zoukankan      html  css  js  c++  java
  • 06 Java字节码的基础知识

    1 字节码命令基础

    1-1 实例分析:构造方法字节码指令(来源)分析

    2a b7 00 01 b1 是字节码指令
    

    需要参考虚拟机手册

    字节码的解释

    总结:使用load指令加载this变量,调用构造方法并将this作为参数,然后返回。

    1) 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
    2) b7 => invokespecial 预备调用构造方法,哪个方法呢?
    3) 00 01 引用常量池中 #1 项,即【 Method java/lang/Object."<init>":()V 】
    4) b1 表示返回
    
    • 上面的信息都可以从手册中查询到

    1-2 实例分析:main方法字节码指令(来源)分析

     b2 00 02 12 03 b6 00 04 b1 是字节码指令
    

    字节码的解释

    总结:首先加载对象:java/lang/System.out:Ljava/io/PrintStream,然后加载参数:”String hello world“,最后调用方法:java/io/PrintStream.println。

    1)b2 => getstatic 用来加载静态变量
    2)00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
    3)12 => ldc 加载参数,哪个参数呢?
    4)03 引用常量池中 #3 项,即 【String hello world】
    5) b6 => invokevirtual 预备调用成员方法,
    6) 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
    7) b1 表示返回
    

    1-3 字节码信息阅读工具:javap 工具

    package part2;
    public class Hello {
        public static void main(String[] args) {
            System.out.println("Hello!");
        }
    }
    
    ClassFile {
        u4             magic;                                 // 前四个字节是魔数,用于表示这个文件是class类型
        u2             minor_version;
        u2             major_version;                         // Java版本信息
        u2             constant_pool_count;                   //  constant_pool_count = 常量池中项目的数目+1 (从01开始算)
        cp_info        constant_pool[constant_pool_count-1];  // 具体的常量池信息 (constant pool information)
        u2             access_flags;                          // 访问修饰,类是公共的还是包私有的
        u2             this_class;                            // 类的类名
        u2             super_class;                           // 父类信息
        u2             interfaces_count;                     
        u2             interfaces[interfaces_count];          // 接口信息
        u2             fields_count;
        field_info     fields[fields_count];                  // 类中变量信息
        u2             methods_count;
        method_info    methods[methods_count];                // 类中方法信息
        u2             attributes_count;
        attribute_info attributes[attributes_count];          // 类附加的属性新
    }
    

    反编译的源码

    javap -v Hello.class                                             // 命令
    Last modified 2021-4-13; size 525 bytes
      MD5 checksum f0f7aa6f9a6a26b5bc2e4b60690a1b1e
      Compiled from "Hello.java"
    public class part2.Hello
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
       #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String             #23            // Hello!
       #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class              #26            // part2/Hello
       #6 = Class              #27            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               Lpart2/Hello;
      #14 = Utf8               main
      #15 = Utf8               ([Ljava/lang/String;)V
      #16 = Utf8               args
      #17 = Utf8               [Ljava/lang/String;
      #18 = Utf8               SourceFile
      #19 = Utf8               Hello.java
      #20 = NameAndType        #7:#8          // "<init>":()V
      #21 = Class              #28            // java/lang/System
      #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
      #23 = Utf8               Hello!
      #24 = Class              #31            // java/io/PrintStream
      #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
      #26 = Utf8               part2/Hello
      #27 = Utf8               java/lang/Object
      #28 = Utf8               java/lang/System
      #29 = Utf8               out
      #30 = Utf8               Ljava/io/PrintStream;
      #31 = Utf8               java/io/PrintStream
      #32 = Utf8               println
      #33 = Utf8               (Ljava/lang/String;)V
    {
      public part2.Hello();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:                                     // 构造方法的字节码操作指令
          stack=1, locals=1, args_size=1         
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V : 根据常量表获得的信息
             4: return
          LineNumberTable:
            line 3: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lpart2/Hello;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:                                     // main方法的字节码操作指令
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #3                  // String Hello!
             5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 5: 0
            line 6: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  args   [Ljava/lang/String;
    }
    SourceFile: "Hello.java"
    

    总结

    • 可以看到该工具的组织形式也是遵照类结构的格式
    • 文件中的注释就是查询常量表的结果

    2 字节码指令,操作数栈,常量池的关系实例

    2-1 实例代码

    package part2;
    public class Hello {
        public static void main(String[] args) {
            int a = 10;
            int b = Short.MAX_VALUE + 1;
            int c = a + b;
            System.out.println(c);
        }
    }
    

    反编译源码

    
      Last modified 2021-4-13; size 589 bytes
      MD5 checksum 2c275d14c3f17dd24f5d406da532ab52
      Compiled from "Hello.java"
    public class part2.Hello
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #7.#25         // java/lang/Object."<init>":()V
       #2 = Class              #26            // java/lang/Short
       #3 = Integer            32768
       #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
       #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
       #6 = Class              #31            // part2/Hello
       #7 = Class              #32            // java/lang/Object
       #8 = Utf8               <init>
       #9 = Utf8               ()V
      #10 = Utf8               Code
      #11 = Utf8               LineNumberTable
      #12 = Utf8               LocalVariableTable
      #13 = Utf8               this
      #14 = Utf8               Lpart2/Hello;
      #15 = Utf8               main
      #16 = Utf8               ([Ljava/lang/String;)V
      #17 = Utf8               args
      #18 = Utf8               [Ljava/lang/String;
      #19 = Utf8               a
      #20 = Utf8               I
      #21 = Utf8               b
      #22 = Utf8               c
      #23 = Utf8               SourceFile
      #24 = Utf8               Hello.java
      #25 = NameAndType        #8:#9          // "<init>":()V
      #26 = Utf8               java/lang/Short
      #27 = Class              #33            // java/lang/System
      #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
      #29 = Class              #36            // java/io/PrintStream
      #30 = NameAndType        #37:#38        // println:(I)V
      #31 = Utf8               part2/Hello
      #32 = Utf8               java/lang/Object
      #33 = Utf8               java/lang/System
      #34 = Utf8               out
      #35 = Utf8               Ljava/io/PrintStream;
      #36 = Utf8               java/io/PrintStream
      #37 = Utf8               println
      #38 = Utf8               (I)V
    {
      public part2.Hello();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 2: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lpart2/Hello;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=4, args_size=1
             0: bipush        10
             2: istore_1
             3: ldc           #3                  // int 32768
             5: istore_2
             6: iload_1
             7: iload_2
             8: iadd
             9: istore_3
            10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
            13: iload_3
            14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
            17: return
          LineNumberTable:
            line 4: 0
            line 5: 3
            line 6: 6
            line 7: 10
            line 8: 17
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      18     0  args   [Ljava/lang/String;
                3      15     1     a   I
                6      12     2     b   I
               10       8     3     c   I
    }
    SourceFile: "Hello.java"
    

    上面的字节码信息会通过类加载器加载到内存中,属于JVM管理的内存的方法区。

    2-2 将常量池与字节码指令放入到方法区

    运行时常量池:本质上是方法区的一部分,对应class文件中的常量池部分。JDK1.8中运行时常量池在采用元空间实现的方法区中,而JDK1.6则是在永久带

    更多参考:

    上图中在运行时常量池中:

    • 第3项是一个数,Integer的数值范围在short的范围内是不会放入到常量池中的,而是和字节码指令放在一起,当超过short范围就会放入常量池,上图中就是在常量池的第3项
    • 第4项是作为引用的成员变量
    • 第5项是方法的引用

    2-3 准备运行main方法,分配栈帧内存

    Code:
          stack=2, locals=4, args_size=1
    
    max_stack:操作数栈的最大深度
    The value of the max_stack item gives the maximum depth of the operand stack of this method at any point during execution of the method.
    
    max_locals:局部变量表的长度(局部变量个数,本例中共有四个,main函数的参数以及a,b,c)
    The value of the max_locals item gives the number of local variables in the local variable array allocated upon invocation of this method (§2.6.1), including the local variables used to pass parameters to the method on its invocation.
    

    main函数的分配的栈帧如上图所示(为main函数的执行做准备):

    • 局部变量表的长度是4,实际上就是为局部变量分配的空间
    • 操作数栈的最大深度是2

    2-4 执行引擎开始执行main方法相关的字节码

    ========================int a = 10 ========================================   
             0: bipush        10    //将数字10压入操作数栈(类型是byte长度会补足到四个字节)
             2: istore_1            //将操作数栈顶数据弹出,存入局部变量表的slot 1即 a= 10
    =======================int b = Short.MAX_VALUE + 1==========================
             3: ldc           #3    // 将运行时常量池的3号位置的数值给加载到栈中
             5: istore_2            // 将栈顶值弹出,放入2号槽位,即 b =  Short.MAX_VALUE + 1;  
    =======================int c = a + b ======================================
             6: iload_1             // 从局部变量表获取a
             7: iload_2             // 从局部变量表获取b
             8: iadd                // 将a与b相加
             9: istore_3            // 将相加结果放入3号槽位,即c = a+b
                 
            10: getstatic     #4    // 获取堆中静态方法PrintStream的引用放入操作数栈中  
            13: iload_3             // 从局部变量表中获取c
            14: invokevirtual #5    // 调用 Method java/io/PrintStream.println:(I)V
            17: return              // 返回
    

    与bipush类似的命令

    sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
    ldc 将一个 int 压入操作数栈
    ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
    
    图解分析



    3 练习:从字节码层面分析自增操作

    3-1 代码

    /**
    * 从字节码角度分析 a++ 相关题目
    */
    public class Demo3_2 {
        public static void main(String[] args) {
            int a = 10;
            int b = a++ + ++a + a--;  //  10 + 12 + 12 = 34
            System.out.println(a);    //  11
            System.out.println(b);    //  34
        }
    }
    

    3-2 字节码

    Last modified 2021-4-13; size 564 bytes
      MD5 checksum 91f804e9ff01612910fcdb6ec6ce3967
      Compiled from "Hello.java"
    public class part2.Hello
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #5.#22         // java/lang/Object."<init>":()V
       #2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = Methodref          #25.#26        // java/io/PrintStream.println:(I)V
       #4 = Class              #27            // part2/Hello
       #5 = Class              #28            // java/lang/Object
       #6 = Utf8               <init>
       #7 = Utf8               ()V
       #8 = Utf8               Code
       #9 = Utf8               LineNumberTable
      #10 = Utf8               LocalVariableTable
      #11 = Utf8               this
      #12 = Utf8               Lpart2/Hello;
      #13 = Utf8               main
      #14 = Utf8               ([Ljava/lang/String;)V
      #15 = Utf8               args
      #16 = Utf8               [Ljava/lang/String;
      #17 = Utf8               a
      #18 = Utf8               I
      #19 = Utf8               b
      #20 = Utf8               SourceFile
      #21 = Utf8               Hello.java
      #22 = NameAndType        #6:#7          // "<init>":()V
      #23 = Class              #29            // java/lang/System
      #24 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
      #25 = Class              #32            // java/io/PrintStream
      #26 = NameAndType        #33:#34        // println:(I)V
      #27 = Utf8               part2/Hello
      #28 = Utf8               java/lang/Object
      #29 = Utf8               java/lang/System
      #30 = Utf8               out
      #31 = Utf8               Ljava/io/PrintStream;
      #32 = Utf8               java/io/PrintStream
      #33 = Utf8               println
      #34 = Utf8               (I)V
    {
      public part2.Hello();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 2: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lpart2/Hello;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=3, args_size=1       
             0: bipush        10           
             2: istore_1
             3: iload_1 
             4: iinc          1, 1                 // iinc 指令是直接在局部变量表的slot 上进行运算
             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
          LineNumberTable:
            line 4: 0
            line 5: 3
            line 6: 18
            line 7: 25
            line 8: 32
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      33     0  args   [Ljava/lang/String;
                3      30     1     a   I
               18      15     2     b   I
    }
    SourceFile: "Hello.java"
    
    • a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc

    3-3 流程图解


    总结

    • 1)变量的自增与自减操作不需要将变量值加载到操作数栈,而2个变量的相加/减则必须先将2个数加载到操作数栈才能实现
    • 2)从字节码的层面看
    a++ + b     // 1)局部变量a被加载到操作数栈。  2)局部表上完成a变量的自增操作  3)局部变量b被加载到操作数栈 4)操作数栈弹出a与b相加,在放入栈。
    ++a + b     // 1)局部表上完成a变量的自增操作 2)局部变量a被加载到操作数栈。   3)局部变量b被加载到操作数栈 4)操作数栈弹出a与b相加,在放入栈。
    

    差异在于自增操作与加载操作数的次序

    4 字节码的条件判断指令和循环控制指令

    4-1 条件判断相关的字节码指令

    字节码 简写 含义
    0x99 ifeq 判断是否 == 0
    0x9a ifne 判断是否 != 0
    0x9b iflt 判断是否 < 0
    0x9c ifge 判断是否 >= 0
    0x9d ifgt 判断是否 > 0
    0x9e ifle 判断是否 <= 0
    0x9f if_icmpeq 两个int是否 ==
    0xa0 if_icmpne 两个int是否 !=
    0xa1 if_icmplt 两个int是否 <
    0xa2 if_icmpge 两个int是否 >=
    0xa3 if_icmpgt 两个int是否 >
    0xa4 if_icmple 两个int是否 <=
    0xa5 if_acmpeq 两个引用是否 ==
    0xa6 if_acmpne 两个引用是否 !=
    0xc6 ifnull 判断是否 == null
    0xc7 ifnonnull 判断是否 != null

    注意点

    • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
    • go to 用来进行跳转到指定行号的字节码

    4-2 条件判断指令实例分析

    代码
    public class Demo3_3 {
        public static void main(String[] args) {
            int a = 0;
            if(a == 0) {
                a = 10;
            } else {
                a = 20;
            }
        }
    }
    
    字节码指令分析
    0:iconst_0           // 得到常量0, -1~5的数
    1: istore_1           // 将0放到常量表的slot 1,即 a = 0注意slot 0是存放args的。
    2: iload_1            // 加载变量a到操作数栈
    3: ifne 12            // 判断操作数栈的数是否不等于0,不等于0,到12位置。
    6: bipush 10          // 将10给放入操作数栈
    8: istore_1           // 将操作数栈中的10放入常量表的slot 1,即 a = 10
    9: goto 15            // 跳转到位置15
    12: bipush 20         // 将20给放入操作数栈
    14: istore_1          // 将操作数栈中的20放入常量表的slot 1,即 a = 20
    15: return
    

    4-3 循环控制指令

    while循环
    public class Demo3_4 {
            public static void main(String[] args) {
                int a = 0;
                while (a < 10) {
                a++;
            }
    	}
    }
    ==========================字节码指令============================================
    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
    
    do while循环
    public class Demo3_5 {
    	public static void main(String[] args) {
    		int a = 0;
            do {
                a++;
            } while (a < 10);
    	}
    }
    =========字节码指令====================
             0: iconst_0
             1: istore_1
             2: iinc          1, 1
             5: iload_1
             6: bipush        10
             8: if_icmplt     2
            11: return
    
    for循环
    public class Demo3_6 {
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
            }
        }
    }
    =========字节码指令====================
             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
    

    总结:可以看到for循环与while循环生成的字节码是相同的

    4-4 练习:字节码角度分析结果为0

    public class Demo3_6_1 {
    public static void main(String[] args) {
            int i = 0;
            int x = 0;
            while (i < 10) {
                x = x++;
                i++;
            } 
            System.out.println(x); // 结果是 0
    	}
    }
    

    分析

    关键在于 x = x++,其中x++先将x送入操作数栈,然后在局部变量表让x自增。然后再将操作数栈的没有自增的x放回局部变量表,所以变量x没有变化。
    

    参考资料

    01 JVM基础课程

    02 JDK8:Chapter 6. The Java Virtual Machine Instruction Set

  • 相关阅读:
    第十周课程总结
    第九周课程总结&实验报告(七)
    第八周课程总结&实验报告(六)
    第七周课程总结&实验报告(五)
    第六周&java实验报告四
    第五周课程总结&试验报告(三)
    课程总结
    第十四周课程总结
    第十三周学习总结
    第十二周编程总结
  • 原文地址:https://www.cnblogs.com/kfcuj/p/14654900.html
Copyright © 2011-2022 走看看