• JVM-Java字节码技术笔记


    Java字节码技术

    Java字节码是java代码编译后的中间代码格式,JVM需要读取并解析字节码才能执行相应的任务

    • 获取字节码简介:由单字节(byte)的指令组成

      • 操作码( 指令), 主要由类型前缀操作名称两部分组成。
      • 根据指令的性质,主要分为四个大类:
        • 栈操作指令,包括与局部变量交互的指令
        • 程序流程控制指令
        • 对象操作指令,包括方法调用指令
        • 算术运算以及类型转换指令
    • 获取字节码清单

      • javap 工具来获取 class 文件中的指令清单,专门用于反编译 class 文件。

      • Compiled from "HelloByteCode.java"
        public class demo.jvm0104.HelloByteCode {
          public demo.jvm0104.HelloByteCode();
            Code:
               0: aload_0
               1: invokespecial #1                  // Method java/lang/Object."":()V
               4: return
        
          public static void main(java.lang.String[]);
            Code:
               0: new           #2                  // class demo/jvm0104/HelloByteCode
               3: dup
               4: invokespecial #3                  // Method "":()V
               7: astore_1
               8: return
        }
        
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
    • 解读字节码清单

      • public demo.jvm0104.HelloByteCode();  // 如果不定义任何构造函数,就会有一个默认的无参构造函数.这是 Java 编译器生成的, 而不是运行时JVM自动生成的。
        
        • 1
      • //每个构造函数中会先调用super类的构造函数,默认构造函数中有些字节码指令来干这个事情
        //解析的java/lang/Object 默认继承了Object类
        public demo.jvm0104.HelloByteCode();
            Code:
               0: aload_0
               1: invokespecial #1                  // Method java/lang/Object."":()V
               4: return
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
    • 查看class中的常量池

      • 常量池就是一个常量的大字典,使用编号的方式把程序里用到的各类常量统一管理起来,这样在字节码操作里,只需要引用编号即可。
      • 大多数时候指的是 运行时常量池。运行时常量池里面的常量主要是由 class 文件中的 常量池结构体 组成的。
      • 查看常量池信息的命令:javap -c -verbose demo.jvm0104.HelloByteCode
        • 反编译class的时候,指定-verbose选项,会输出附加信息
      • #1 = Methodref #4.#13 // java/lang/Object."":()V,
      • #1:常量编号,该文件中其他地方可以引用
      • = :分隔符
      • Methodref:表明这个常量指向的是一个方法;具体是哪个类的哪个方法呢? 类指向的 #4, 方法签名指向的 #13;
    • 查看方法信息

      •  //main方法编译结果
         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
        
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
      • 方法描述: ([Ljava/lang/String;)V:
        
        • 1
        • 小括号内是入参信息/形参信息;
        • 左方括号表述数组;
        • L 表示对象;
        • 后面的java/lang/String就是类名称;
        • 小括号后面的 V 则表示这个方法的返回值是 void
        • 方法的访问标志也很容易理解 flags: ACC_PUBLIC, ACC_STATIC,表示 public 和 static。
      • 还可以看到执行该方法时需要的栈(stack)深度是多少,需要在局部变量表中保留多少个槽位, 还有方法的参数个数: stack=2, locals=2, args_size=1

    • 线程栈与字节码执行模型

      • 每个线程都有一个独属于自己的线程栈(JVM stack),用于存储栈帧(Frame)。每一次方法调用,JVM都会自动创建一个栈帧。
        • 栈帧操作数栈局部变量数组 以及一个class 引用组成。class 引用 指向当前方法在运行时常量池中对应的 class)。
      • 局部变量数组 也称为 局部变量表(LocalVariableTable), 其中包含了方法的参数,以及局部变量。 局部变量数组的大小在编译时就已经确定: 和局部变量+形参的个数有关,还要看每个变量/参数占用多少个字节。操作数栈是一个 LIFO 结构的栈, 用于压入和弹出值。 它的大小也在编译时确定。
      • 有一些操作码/指令可以将值压入“操作数栈”; 还有一些操作码/指令则是从栈中获取操作数,并进行处理,再将结果压入栈。操作数栈还用于接收调用其他方法时返回的结果值。
    • 方法体中的字节码解读

      •          0: new #2 // class demo/jvm0104/HelloByteCode
                 3: dup
                 4: invokespecial #3 // Method "":()V
                 7: astore_1
                 8: return
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • 前面的数字:间隔不相等的原因是, 有一部分操作码会附带有操作数, 也会占用字节码数组中的空间。

      • 例如:new 就会占用三个槽位: 一个用于存放操作码指令自身,两个用于存放操作数。因此,下一条指令 dup 的索引从 3 开始。

    • 对象初始化指令:new 指令, init 以及 clinit 简介

      • 创建类实例生成操作码

      • 0: new #2 // class demo/jvm0104/HelloByteCode   创建对象,但没有调用构造函数
        3: dup    //   用来调用某些特殊方法的,即构造函数
        4: invokespecial #3 // Method "":()V  用于复制栈顶的值。构造函数调用不会返回值,所以如果没有 dup 指令, 在对象上调用方法并初始化之后,操作数栈就会是空的,在初始化之后就会出问题。所以在构造函数返回之后,可以将对象实例赋值给局部变量或某个字段
        
        • 1
        • 2
        • 3
      • 接下来指令

      • astore {N} or astore_{N} – 赋值给局部变量,其中 {N} 是局部变量表中的位置。
        putfield – 将值赋给实例字段
        putstatic – 将值赋给静态字段
        
        • 1
        • 2
        • 3
      • 在调用构造函数的时候,还会执行另一个类似的方法 ,甚至在执行构造函数之前就执行了。

      • 还有一个可能执行的方法是该类的静态初始化方法 , 但 并不能被直接调用,而是由这些指令触发的: new, getstatic, putstatic or invokestatic

    • 栈内存操作指令

      • 最基础的是 duppop 指令。
        • dup 指令复制栈顶元素的值。
        • pop 指令则从栈中删除最顶部的值。
      • 复杂一点的指令:比如,swap, dup_x1dup2_x1
        • swap 指令可交换栈顶两个元素的值,例如A和B交换位置(图中示例4);
        • dup_x1 将复制栈顶元素的值,并在栈顶插入两次(图中示例5);
        • dup2_x1 则复制栈顶两个元素的值,并插入第三个值(图中示例6)。
      • dup 指令:复制栈顶的值,并将复制的值压入栈。
      • dup_x1 指令:复制栈顶的值,并将复制的值插入到最上面 2 个值的下方。
      • dup2_x1 指令:复制栈顶 1 个 64 位/或 2 个 32 位的值, 并将复制的值按照原始顺序,插入原始值下面一个 32 位值的下方。
    • 局部变量表

      • stack 主要用于执行指令,而局部变量则用来保存中间结果,两者之间可以直接交互。

      • javac -g demo/jvm0104/*.java(生成调试信息的 -g 参数)

      • javap -c -verbose demo/jvm0104/LocalVariableTest (反编译)

      • 代码

      • //移动平均数
        public class MovingAverage {
            private int count = 0;
            private double sum = 0.0D;
            public void submit(double value){
                this.count ++;
                this.sum += value;
            }
            public double getAvg(){
                if(0 == this.count){ return sum;}
                return this.sum/this.count;
            }
        }
        
        public class LocalVariableTest {
            public static void main(String[] args) {
                MovingAverage ma = new MovingAverage();
                int num1 = 1;
                int num2 = 2;
                ma.submit(num1);
                ma.submit(num2);
                double avg = ma.getAvg();
            }
        }
        
        • 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=3, locals=6, args_size=1
                 0: new           #2                  // class demo/jvm0104/MovingAverage  new, 创建 MovingAverage 类的对象;
                 3: dup //  复制栈顶引用值。
                 4: invokespecial #3                  // Method  demo/jvm0104/MovingAverage."":()V   invokespecial 执行对象初始化。
                 7: astore_1 //使用 astore_1 指令将引用地址值(addr.)存储(store)到编号为1的局部变量中: astore_1 中的 1 指代 LocalVariableTable 中ma对应的槽位编号,
                 8: iconst_1  // iconst_1 和 iconst_2 用来将常量值1和2加载到栈里面, 并分别由指令 istore_2 和 istore_3 将它们存储到在 LocalVariableTable 的槽位 2 和槽位 3 中。store 之类的指令调用实际上从栈顶删除了一个值。 这就是为什么再次使用相同值时,必须再加载(load)一次的原因。
                 9: istore_2
                10: iconst_2
                11: istore_3
                12: aload_1
                13: iload_2
                14: i2d
                15: invokevirtual #4                  // Method demo/jvm0104/MovingAverage.submit:(D)V
                18: aload_1
                19: iload_3
                20: i2d
                21: invokevirtual #4                  // Method demo/jvm0104/MovingAverage.submit:(D)V
                24: aload_1 //调用 getAvg() 方法后,返回的结果位于栈顶,然后使用 dstore 将 double 值保存到本地变量4号槽位,这里的d表示目标变量的类型为double。
                25: invokevirtual #5                  // Method demo/jvm0104/MovingAverage.getAvg:()D
                28: dstore        4
                30: return
              LineNumberTable:
                line 5: 0
                line 6: 8
                line 7: 10
                line 8: 12
                line 9: 18
                line 10: 24
                line 11: 30
              LocalVariableTable:
                Start  Length  Slot  Name   Signature
                    0      31     0  args   [Ljava/lang/String;
                    8      23     1    ma   Ldemo/jvm0104/MovingAverage;
                   10      21     2  num1   I
                   12      19     3  num2   I
                   30       1     4   avg   D
        
        • 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
      • 给局部变量赋值时,需要使用相应的指令来进行 store,如 astore_1store 类的指令都会删除栈顶值。 相应的 load 指令则会将值从局部变量表压入操作数栈,但并不会删除局部变量中的值。

    • 流程控制指令

      • 主要是分支和循环在用, 根据检查条件来控制程序的执行流程。

      • 代码

      • public class ForLoopTest {
            private static int[] numbers = {1, 6, 8};
            public static void main(String[] args) {
                MovingAverage ma = new MovingAverage();
                for (int number : numbers) {
                    ma.submit(number);
                }
                double avg = ma.getAvg();
            }
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
      • 编译反编译

      • javac -g demo/jvm0104/*.java
        javap -c -verbose demo/jvm0104/ForLoopTest
        
        • 1
        • 2
      • 字节码

      • 0: new           #2                  // class demo/jvm0104/MovingAverage
        3: dup
        4: invokespecial #3                  // Method demo/jvm0104/MovingAverage."":()V
        7: astore_1
        8: getstatic     #4                  // Field numbers:[I
        11: astore_2
        12: aload_2
        13: arraylength
        14: istore_3
        15: iconst_0
        16: istore        4
                18: iload         4  //循环体 用于执行循环计数器与数组长度的比较
                20: iload_3
                21: if_icmpge     43 //if, integer, compare, great equal, 如果一个数的值大于或等于另一个值,则程序执行流程跳转到pc=43的地方继续执行。
                24: aload_2
                25: iload         4
                27: iaload
                28: istore        5
                30: aload_1
                31: iload         5
                33: i2d
                34: invokevirtual #5                  // Method demo/jvm0104/MovingAverage.submit:(D)V  
                37: iinc          4, 1   // 4号槽位的值加1
                40: goto          18   //跳到循环开始的地方
                43: aload_1
                44: invokevirtual #6                  // Method demo/jvm0104/MovingAverage.getAvg:()D
                47: dstore_2
                48: return
              LocalVariableTable:
                Start  Length  Slot  Name   Signature
                   30       7     5 number   I  //5 号槽位被 number 占用了。
                    0      49     0  args   [Ljava/lang/String;   //0槽位被 main 方法的参数 args 占据了
                    8      41     1    ma   Ldemo/jvm0104/MovingAverage; //1 号槽位被 ma 占用了。
                   48       1     2   avg   D //2 号槽位是for循环之后才被 avg 占用的。
                   
                   
                   
        2号槽位的变量保存了 numbers 的引用值,占据了 2号槽位。
        3号槽位的变量, 由 arraylength 指令使用, 得出循环的长度。
        4号槽位的变量, 是循环计数器, 每次迭代后使用 iinc 指令来递增。
        
        
        • 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
    • 算术运算指令与类型转换指令

      • int 值作为参数传递给实际上接收 doublesubmit() 方法时, 在实际调用该方法之前,使用了类型转换的操作码

      •      31: iload         5
                33: i2d
                34: invokevirtual #5                  // Method demo/jvm0104/MovingAverage.submit:(D)V
        
        • 1
        • 2
        • 3
      • 将一个 int 类型局部变量的值, 作为整数加载到栈中,然后用 i2d 指令将其转换为 double 值,以便将其作为参数传给submit方法。

      • 唯一不需要将数值load到操作数栈的指令是 iinc,它可以直接对 LocalVariableTable 中的值进行运算。 其他的所有操作均使用栈来执行。

    • 方法调用指令和参数传递

      用于方法调用的指令

      • invokestatic,用于调用某个类的静态方法,这也是方法调用指令中最快的一个。

      • invokespecial, 用来调用构造函数,也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。

      • invokevirtual,如果是具体类型的目标对象,用于调用公共,受保护和打包私有方法。

      • invokeinterface,调用的方法属于某个接口。运行时受到更多限制

        区别:

      • 使用 invokestatic 指令,JVM 就确切地知道要调用的是哪个方法:因为调用的是静态方法,只能属于一个类。

      • 使用 invokespecial 时, 查找的数量也很少, 解析也更加容易, 那么运行时就能更快地找到所需的方法。

    • JDK7 新增的方法调用指令 invokedynamic

      • 是实现“动态类型语言”
      • 在不改变字节码的时候,Java 语言层面想调用一个类 A 的方法 m,只有两个办法:
        • 使用A a=new A(); a.m(),拿到一个 A 类型的实例,然后直接调用方法;
        • 通过反射,通过 A.class.getMethod 拿到一个 Method,然后再调用这个Method.invoke反射调用;
      • invokedynamic配合新增的方法句柄(Method Handles,可以用来描述一个跟类型 A 无关的方法 m 的签名,甚至不包括方法名称,这样就可以做到我们使用方法 m 的签名,但是直接执行的时候调用的是相同签名的另一个方法 b),可以在运行时再决定由哪个类来接收被调用的方法。在此之前,只能使用反射来实现类似的功能。该指令使得可以出现基于 JVM 的动态语言,让 jvm 更加强大。而且在 JVM 上实现动态调用机制,不会破坏原有的调用机制。这样既很好的支持了 Scala、Clojure 这些 JVM 上的动态语言,又可以支持代码里的动态 lambda 表达式。
  • 相关阅读:
    7 SpringBoot与Elasticsearch
    小熊家务帮day8-day9 客户管理模块2 (用户定位,地址簿,实名认证,银行卡信息上传等功能)
    树形dp题单训练
    计算机网络第一章笔记
    如何设计分布式系统-分布式事务-XA?
    【Leetcode】2422. Merge Operations to Turn Array Into a Palindrome
    2024年最新 CKA 试题题库及答案详解 导航页
    Chat GPT:智能对话的下一步
    最简单入门的SpringBoot+Vue项目用WebSoket完整教程代码,做客服,即时通信的看过来!
    编程初学者应该先学C++、Java还是Python?
  • 原文地址:https://blog.csdn.net/weixin_39795049/article/details/133147483