java字节码技术

1. Java字节码简介

Java bytecode 由单字节( byte )的指令组成, 理论上最多支持 256 个操作码(opcode)。实际上Java只使用了200左右的操作码, 还有一些操作码则保留给调试操作。
操作码, 主要由 类型前缀操作名称 两部分组成。

例如,’ i ’ 前缀代表 ‘ integer ’,所以,’ iadd ’ 很容易理解, 表示对整-- 数执行加 法运算。

根据指令的性质,主要分为四个大类:

  1. 栈操作指令,包括与局部变量交互的指令
  2. 程序流程控制指令
  3. 对象操作指令,包括方法调用指令
  4. 算术运算以及类型转换指令

此外还有一些执行专门任务的指令,比如同步(synchronization)指令,以及抛出异常相关的指令等等。下文会对这些指令进行详细的讲解。

2. 获取字节码清单

  1. javac编译,生成字节码
  2. 可以用javap工具来获取 class 文件中的指令清单

javac 不指定 ‐d 参数编译后生成的 .class 文件默认和源代码在同一个目录。
注意: javac 工具默认开启了优化功能, 生成的字节码中没有局部变量表 (LocalVariableTable),相当于局部变量名称被擦除。如果需要这些调试信息, 在编 译时请加上 ‐g 选项。
JDK自带工具的详细用法, 请使用: javac ‐help 或者 javap ‐help 来查看; 其他类似。

3. 解读字节码

java类中不定义任何构造函数,就会有一个默认的午餐早函数。在编译后,我们可以在对应的class文件中看到这个默认的无参构造函数。

Compiled from "HelloByteCode.java"
public class com.megetood.geek.week01.HelloByteCode {
  public com.megetood.geek.week01.HelloByteCode();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/megetood/geek/week01/HelloByteCode
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: return
}

4. 查看class文件中的常量池信息

常量池: Constant pool, 大多数时候 指的是 运行时常量池 。运行时的常量池主要就是由 class 文件中的 常量池结构体 组成的

在反编译 class 时,指定 ‐verbose 选项, 则会 输出附加信息 ,可一看到更详细的字节码信息。

Classfile /D:/worksapce/ideaspace/enhance/megetood-geek/src/main/java/com/megetood/geek/week01/HelloByteCode.class
  Last modified 2020-10-17; size 313 bytes
  MD5 checksum a2c625e7a3caa197a59067d4a287f092
  Compiled from "HelloByteCode.java"
public class com.megetood.geek.week01.HelloByteCode
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = Class              #14            // com/megetood/geek/week01/HelloByteCode
   #3 = Methodref          #2.#13         // com/megetood/geek/week01/HelloByteCode."<init>":()V
   #4 = Class              #15            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               SourceFile
  #12 = Utf8               HelloByteCode.java
  #13 = NameAndType        #5:#6          // "<init>":()V
  #14 = Utf8               com/megetood/geek/week01/HelloByteCode
  #15 = Utf8               java/lang/Object
{
  public com.megetood.geek.week01.HelloByteCode();
    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 7: 0

  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
         0: new           #2                  // class com/megetood/geek/week01/HelloByteCode
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
}
SourceFile: "HelloByteCode.java"

其中显示了很多关于class文件信息: 编译时间, MD5校验和, 从哪个 .java 源文 件编译得来,符合哪个版本的Java语言规范。

到 ACC_PUBLIC(public) 和 ACC_SUPER (super)访问标志符

Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = Class              #14            // com/megetood/geek/week01/HelloByteCode
   #3 = Methodref          #2.#13         // com/megetood/geek/week01/HelloByteCode."<init>":()V
   #4 = Class              #15            // java/lang/Object

#1 = Methodref #4.#13 // java/lang/Object."<init>":()V

  • #1 常量编号, 该文件中其他地方可以引用。
  • = 等号就是分隔符.
  • Methodref 表明这个常量指向的是一个方法;具体是哪个类的哪个方法呢? 类 指向的 #4 , 方法签名指向的 #13 ; 当然双斜线注释后面已经解析出来可读性比 较好的说明了。

5. 查看方法信息

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

([Ljava/lang/String;)V:方法描述

  • 其中小括号内是入参信息/形参信息,
  • 左方括号表述数组,
  • L 表示对象,
  • 后面的 java/lang/String 就是类名称
  • 小括号后面的 V 则表示这个方法的返回值是 void
  • 方法的访问标志也很容易理解 flags: ACC_PUBLIC, ACC_STATIC ,表示 public和static

stack=2, locals=2, args_size=1 分别表示栈(stack)深度是多少,需要在局部变量表中保留多少 个槽位, 还有方法的参数个数

  public com.megetood.geek.week01.HelloByteCode();
    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

这里无参构造函数的参数个数不是0: stack=1, locals=1, args_size=1;这是因为在 Java 中, 如果是静态方法则没有 this 引用。 对于非静态方法, this 将被分配到局部变量表的第0号槽位中,

6. 线程栈与字节码执行模型

JVM是一台基于栈的计算机器。每个线程都有一个独属于自己的线程栈(JVM stack), 用于存储 栈帧 (Frame)。每一次方法调用,JVM都会自动创建一个栈帧。 栈帧操作数栈局部变量数组 以及一个 class引用 组成。

  • class引用 指向当前方法 在运行时常量池中对应的class)。
  • 局部变量表 (LocalVariableTable), 其中包含了方法的参数, 以及局部变量。
  • 操作数栈是一个LIFO结构的栈, 用于压 入和弹出值。 它的大小也在编译时确定。 有一些操作码/指令可以将值压入操作数栈; 还有一些操作码/指令则是从栈中获取操作数,并进行处理,再将结果压入栈。操作数栈还用于接收调用其他方法时返回的结 果值
    在这里插入图片描述

7. 方法体中的字节码

下面这个是上面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
         0: new           #2                  // class com/megetood/geek/week01/HelloByteCode
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8

指令前面的数字代表的是操作数栈中所占的槽位,new就会占用三个槽位: 一个用于存放操作码指令自身,两个用于存放操作数。可以看到new是在0号槽位,而他的下一条指令dup从3号位开始。

8. 对象初始化指令:new指令, init 以及 clinit

	     0: new           #2                  // class com/megetood/geek/week01/HelloByteCode
         3: dup
         4: invokespecial #3                  // Method "<init>":()V

new, dup 和 invokespecial 指令在一起时,那么一定是在创建类 的实例对象!

  • new 指令只是创建对象,但没有调用构造函数。
  • invokespecial 指令用来调用某些特殊方法的, 当然这里调用的是构造函数。
  • dup 指令用于复制栈顶的值。 由于构造函数调用不会返回值,所以如果没有dup指令, 在对象上调用方法并初始 化之后,操作数栈就会是空的,在初始化之后就会出问题, 接下来的代码就无法 对其进行处理

9. 栈内存操作指令

  • dup 指令复制栈顶元素的值。
  • pop 指令则从栈中删除最顶部的值

10. 局部变量表

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

/**
 * 计算平均数
 * /
public class MovingAverage {
    private int count = 0;
    private double sum = 0.0D;

    /**
     * 增加一个数
     *
     * @param val
     */
    public void submit(double val) {
        this.count++;
        this.sum += val;
    }

    /**
     * 获得平均值
     */
    public double getAvg() {
        if (0 == count) {
            return sum;
        }
        return this.sum / this.count;
    }
}

调用类

public class LocalVariableTest {

    /*
        计算了num1与num2的平均数
     */
    public static void main(String[] args) {
        MovingAverage movingAverage = new MovingAverage();
        int num1 = 1;
        int num2 = 2;
        movingAverage.submit(num1);
        movingAverage.submit(num2);
        double avg = movingAverage.getAvg();
    }
}

对这两个类同时编译,然后反编译得到对应字节码,其中mian方法的字节码如下:

public static void main(java.lang.String[]);
    Code:
    // 创建对象
       0: new           #2                  // class com/megetood/geek/week01/codebyte/MovingAverage
    // 赋值栈顶引用值
       3: dup
    // 调用构造函数,初始化
       4: invokespecial #3                  // Method com/megetood/geek/week01/codebyte/MovingAverage."<init>":()V
    // 将新建的对象引用地址值存储到局部变量表索引为1的位置
       7: astore_1
    // 将常量值1压栈
       8: iconst_1
    // 将常量1存储到局部变量表索引为2的位置
       9: istore_2
    // 将常量值2压栈
      10: iconst_2
    // 将常量1存储到局部变量表索引为3的位置
      11: istore_3
    // 将局部变量表索引为1、2的值压栈,分别对应前面新建的MovingAverage对象和int值1
      12: aload_1
      13: iload_2
    // 对int值1进行类型转换,转换成double类型
      14: i2d
    // 调用movingAverage对象的submit方法
      15: invokevirtual #4                  // Method com/megetood/geek/week01/codebyte/MovingAverage.submit:(D)V
    // 这三行跟上面是同样的操作
      18: aload_1
      19: iload_3
      20: i2d
      21: invokevirtual #4                  // Method com/megetood/geek/week01/codebyte/MovingAverage.submit:(D)V
      24: aload_1
      25: invokevirtual #5                  // Method com/megetood/geek/week01/codebyte/MovingAverage.getAvg:()D
    // 将25行函数调用结果存储到局部变量表索引为4的位置
      28: dstore        4
    // 返回栈顶值
      30: return
}

在这里插入图片描述

11. 流程控制指令

在上面基础上新建下面这个类

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();
    }
}

在该类所在目录下执行下面命令:

javac -g *.java
javap -c -verbose ForLoopTest.class

得到下面字节码(这里只展示了main方法中的)

0: new           #2                  // class com/megetood/geek/week01/codebyte/MovingAverage
       3: dup
       4: invokespecial #3                  // Method com/megetood/geek/week01/codebyte/MovingAverage."<init>":()V
       7: astore_1
    // 获取数组
       8: getstatic     #4                  // Field numbers:[I
    // 保存局部对象
      11: astore_2
    // 加载刚刚保存的
      12: aload_2
    // 将数组长度压栈
      13: arraylength
    // 保存数组长度值到局部变量表3号位置
      14: istore_3
    // 循环计数器,i
      15: iconst_0
      16: istore        4
    // 获取计数器初始值压栈
      18: iload         4
    // 将数组长度压栈
      20: iload_3
    // 比较计数器值与数组长度,如果计数器值>=数组长度,则跳到中间的指令,到43行开始执行
      21: if_icmpge     43
    // 下面是ma.submit(number);这条指令的字节码
      24: aload_2
      25: iload         4
      27: iaload
      28: istore        5
      30: aload_1
      31: iload         5
      33: i2d
      34: invokevirtual #5                  // Method com/megetood/geek/week01/codebyte/MovingAverage.submit:(D)V
    // 计数器自增
      37: iinc          4, 1
    // 跳到18行继续执行
      40: goto          18
    // 下面是double avg = ma.getAvg();这条指令的字节码
      43: aload_1
      44: invokevirtual #6                  // Method com/megetood/geek/week01/codebyte/MovingAverage.getAvg:()D
      47: dstore_2
      48: return
    // 这里就是局部变量表了
 	LocalVariableTable:
        Start  Length  Slot  Name   Signature
           30       7     5 number   I
            0      49     0  args   [Ljava/lang/String;
            8      41     1    ma   Lcom/megetood/geek/week01/codebyte/MovingAverage;
           48       1     2   avg   D

12. 算术运算指令与类型转换指令

数据类型转换

算术操作码和类型
在这里插入图片描述

类型转换操作码
在这里插入图片描述

13. 方法调用指令和参数传递

  • invokestatic,顾名思义,这个指令用于调用某个类的静态方法,这也是方法 调用指令中最快的一个。
  • invokespecial , 我们已经学过了, invokespecial 指令用来调用构造函数, 但也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。
  • invokevirtual,如果是具体类型的目标对象, invokevirtual 用于调用公 共,受保护和打包私有方法。
  • invokeinterface ,当要调用的方法属于某个接口时,将使用 invokeinterface 指令

invokinterface相比,invokevirtual 针对具体的类型方法表是固定的,所 以每次都可以精确查找,效率更高

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值