【JVM 】Java字节码

什么是字节码?

Java bytecode由单字节(byte)的指令组成,理论上最多支持256个操作码(opcode)。实际上Java只使用了200左右的操作码,还有一些操作码则保留给调试操作。
根据指令的性质,主要分为四个大类:

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

生成字节码

假如一个类源代码如下:

public class Hello {
    public static void main(String[] args) {
        Hello hello = new Hello();
    }
}

编译:javac .../Hello.java
查看字节码:javap -c ...Hello
包含常量池的字节码: javap -c -verbose ...Hello

结果如下:

Classfile /G:/zkml/JavaCourseCodes/out/JavaCourseCodes/jvm/Hello.class
  Last modified 2021-9-18; size 401 bytes
  MD5 checksum 6b357e314b11531f59f717ae55471fc8
  Compiled from "Hello.java"
public class jvm.Hello
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#19         // java/lang/Object."<init>":()V
   #2 = Class              #20            // jvm/Hello
   #3 = Methodref          #2.#19         // jvm/Hello."<init>":()V
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               Ljvm/Hello;
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               args
  #15 = Utf8               [Ljava/lang/String;
  #16 = Utf8               hello
  #17 = Utf8               SourceFile
  #18 = Utf8               Hello.java
  #19 = NameAndType        #5:#6          // "<init>":()V
  #20 = Utf8               jvm/Hello
  #21 = Utf8               java/lang/Object
{
  public jvm.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   Ljvm/Hello;

  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 jvm/Hello
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
            8       1     1 hello   Ljvm/Hello;
}
SourceFile: "Hello.java"


简单说明

可以看到使用-verbose打印的信息比较详细,包含了最后修改时间,版本号(major version: 52代表的是JDK8),文件大小,文件名,访问标志flags: ACC_PUBLIC, ACC_SUPER

Constant pool 代表的是常量池
#1 = Methodref #4.#19:#1的常量是由#4和#19组成的,#4对应#21,#21就是Object,就是当前类的父类是Object;#19表示一个方法名,由#5和#6组成,表示是一个初始化方法,并且是()V 代表void的类型。所以上面所有的合起来就是后面注释描述的那样,是一个返回类型是void的构造函数

再往下可以看到有两块方法的代码,其中public jvm.Hello();代码当前这个类默认的无参构造方法,第二个public static void main(java.lang.String[]);对应代码了写的main方法。代码块里面的new、aload_0、retrun等就是java的字节码的操作指令。当这些指令加载到虚拟机,虚拟机就会按顺序执行只写操作指令。
比如0: aload_0,前面的0是具体的偏移码,表示在这段二进制里面的偏移量为0,aload_0是一个栈操作的指令
在这里插入图片描述
load表示从局部方发表把变量加载到操作栈
store表示从栈上把结果存回本剧的变量表
aload_0就表示把本地变量表里的第0个位置的变量加载到粘上来,前缀a表示这个变量是一个引用类型
1: invokespecial #1偏移量1的这个指令,后面有个注释,表示初始化Object方法,#1对应的是常量池的常量。并且这个操作指令占用三个字节码(包含两个操作数),所以可以看到下面的return的操作指令前面的偏移量是4

第二块代码块中的指令,首先看stack=2, locals=2, args_size=1,这个代表JVM需要的栈的深度是2,本地变量表的长度也是2,参数数量是一个。所以在编译好之后,这些数值就已经固定了。然后下面偏移量为0的new后面有个#2,看后面的注释可以知道是当前的Hello类,表示的就是初始化这个类,变成一个对象,new占用三个字节码。后面偏移量为3的dup表示压栈,后面再调用invokespecial,调用构造函数,实例化方法。在后面astrore_1就是把这个对象的引用压到本地变量表的标号为1的位置上。最后return

字节码的运行时结构

在这里插入图片描述
JVM 是一台基于栈的计算机器。
每个线程都有一个独属于自己的线程栈(JVM Stack),用于存储栈帧(Frame)。
每一次方法调用、JVM 都会自动创建一个栈帧。
栈帧由操作数栈、 局部变量数组以及一个 Class 引用组成。
Class 引用指向当前方法在运行时常量池中对应的 Class。

从助记符到二进制在这里插入图片描述

带有运算的字节码部分解析

定义两个类分别如下:

public class MovingAverage {
    private int count = 0;
    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 m = new MovingAverage();
        int num1 = 1;
        int num2 = 2;
        m.submit(num1);
        m.submit(num2);
        double avg = m.getAvg();
    }
}

使用javap命令查看LocalVariableTest 得到字节码文件,我们只看下面部分:

         0: new           #2                  // class jvm/MovingAverage
         3: dup
         4: invokespecial #3                  // Method jvm/MovingAverage."<init>":()V
         7: astore_1
         8: iconst_1
         9: istore_2
        10: iconst_2
        11: istore_3
        12: aload_1
        13: iload_2
        14: i2d
        15: invokevirtual #4                  // Method jvm/MovingAverage.submit:(D)V
        18: aload_1
        19: iload_3
        20: i2d
        21: invokevirtual #4                  // Method jvm/MovingAverage.submit:(D)V
        24: aload_1
        25: invokevirtual #5                  // Method jvm/MovingAverage.getAvg:()D
        28: dstore        4
        30: return

      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1     m   Ljvm/MovingAverage;
           10      21     2  num1   I
           12      19     3  num2   I
           30       1     4   avg   D

invokespecial #3 创建MovingAverage对象
astore_1 把上面的对象放到本地变量表的1位置
iconst_1 压栈一个int类型的常量num1
istore_2 放到本地变量表的2位置,slot = 2
iconst_2 压栈一个int类型的常量num2
istore_3 放到本地变量表的3位置,slot = 2
aload_1 把astore_1的本地变量表里的m加载到栈上
iload_2 把istore_2 的本地变量表里的加num1载到栈上
i2d 转换类型,把int类型转换成double类型
invokevirtual #4 执行 submit 方法
后面类似

循环控制的字节码部分解析

我们定义一个简单的带有循环控制的类:

public class ForLoopTest {
    private static int[] numbers = {1, 6, 8};

    public static void main(String[] args) {
        MovingAverage movingAverage = new MovingAverage();
        for (int n : numbers) {
            movingAverage.submit(n);
        }
        double avg = movingAverage.getAvg();
    }
}

使用javap命令查看得到字节码文件,我们只看下面部分:

        14: istore_3
        15: iconst_0
        16: istore        4
        18: iload         4
        20: iload_3
        21: if_icmpge     43
        24: aload_2
        25: iload         4
        27: iaload
        28: istore        5
        30: aload_1
        31: iload         5
        33: i2d
        34: invokevirtual #5                  // Method jvm/MovingAverage.submit:(D)V
        37: iinc          4, 1
        40: goto          18

if_icmpge 和 goto 完成了整个流程的控制
if_icmpge 43 表示比较int类型变量,ge表示>= 意思就是如果前面的数大于等于后面的数,那么就跳转到43行,我们可以看到goto所在的行是40,所以说上面满足条件之后就跳出循环了,不满足就往下执行。到了goto,就进行下一次的循环

方法调用的指令

Invokestatic: 顾名思义,这个指令用于调用某个类的静态方法,这是方法调用指令中最快
的一个。
Invokespecial : 用来调用构造函数,但也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。
invokevirtual : 如果是具体类型的目标对象,invokevirtual 用于调用公共、受保护和
package 级的私有方法。
invokeinterface : 当通过接口引用来调用方法时,将会编译为 invokeinterface 指令。
invokedynamic : JDK7 新增加的指令,是实现“动态类型语言”(Dynamically Typed
Language)支持而进行的升级改进,同时也是 JDK8 以后支持 lambda 表达式的实现基础。

一个动态的例子

代码如下:

public class Demo {
    public static void test() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 5;
    }
}

在这里插入图片描述

这里是引用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值