Java字节码执行

kolbe 2021年09月21日 189次浏览

1 栈帧结构

Java虚拟机以方法作为基本执行单元,栈帧则是支持方法调用和执行的数据结构,它是虚拟机栈的元素。栈帧由以下几部分组件

1.1 局部变量表

局部变量表以变量槽为单位,存放着boolean、byte、char、short、int、float、reference、returnAddress类型的数据,一般变量槽大小于32位,如果在存放long和double,则需要使用两个变量槽。
为了节省内存空间,变量槽支持复用,如果某个变量离开了作用域,该变量槽将可以给其它变量使用。因为存在着复用,可能影响垃圾回收的行为。

1.2 操作数栈

操作数栈用来存放方法运行过程中,各种字节码指令写入和提取的内容,也就是出栈和入栈。

1.3 动态连接

每个栈帧中包含一个指向运行时常量池的该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

1.4 方法返回地址

方法开始执行后,有两种方式退出方法,一种是遇到方法返回的字节码指令,一种是执行过程遇到异常,在方法退出后,需要返回方法调用的位置,需要栈帧保存一些信息(PC计数器),用来恢复上层的方法执行状态。
方法退出(当前栈帧出栈)要执行以下操作:

  • 恢复上层方法的局部变量表和操作数栈
  • 把返回值压入调用者栈帧的操作数栈
  • 调整PC计数器指向方法的调用指令后一条指令

2 方法调用

方法调用是确定被调用方法的版本,暂不涉及具体的运行。

2.1 解析

在类加载的解析阶段,会将编译期就能确定调用版本的符号引用转化成直接引用,与之相对应的字节码指令:

  • invokestatic:静态方法
  • invokespecial:调用实例构造器、私有方法、父类方法
    静态方法、实例构造器、私有方法、父类方法统称为非虚方法

2.2 分派

2.2.1 静态分派(重载)

静态分派示例:

public class StaticDispatch {
  static abstract class Human {}
  static class Man extends Human {}
  static class Woman extends Human {}
  public void sayHello(Human guy) {
    System.out.println("Hello, Guy");
  }
  public void sayHello(Man guy) {
    System.out.println("Hello, Gentleman");
  }
  public void sayHello(Woman guy) {
    System.out.println("Hello, Lady");
  }
  public static void main(String[] args) {
    Human man = new Man();
    Human woman = new Woman();
    StaticDispatch sd = new StaticDispatch();
    sd.sayHello(man);
    sd.sayHello(woman);
  }
}

// 输出结果
// Hello, Guy
// Hello, Guy
Human man = new Man();

其中Human称为变量的静态类型,Man称为变量的实际类型,变量的静态类型和实际类型变化示例:

实际类型的变化

Human man = new Man();
man = new Woman();

静态类型的变化

sd.sayHello((Man)man);
sd.sayHello((Woman)woman);

两者区别在于静态类型本身不会变化,仅仅在使用那一刻会发生变化;静态类型在编译期就确定下来,实际类型在运行期才能确定,编译器在重载时是通过参数的静态类型来确定使用的方法重载版本。所有依赖静态类型来定位方法的分派动作称为静态分派。

2.2.2 动态分派(多态)

动态分派示例:

public class DynamicDispatch {
  static abstract class Human {
    protected abstract void sayHello();
  }
  static class Man extends Human {
    @Override
    protected void sayHello() {
      System.out.println("man say hello");
    }
  }
  static class Woman extends Human {
    @Override
    protected void sayHello() {
      System.out.println("woman say hello");
    }
  }
  public static void main(String[] args) {
    Human man = new Man();
    Human woman = new Woman();
    man.sayHello();
    woman.sayHello();
    man = new Woman();
    man.sayHello();
  }
}

// 输出结果
// man say hello
// woman say hello
// woman say hello
$ javap -c -l DynamicDispatch.class

注:
javap是jdk自带的反编译工具,根据class字节码文件,解析出当前类的code区、本地变量表、异常表、代码行、常量池等
javap -l 输出行号和本地变量表信息
javap -c 对当前class文件进行反编译生成汇编代码

public class net.kolbe.java.classexcute.DynamicDispatch {
  public net.kolbe.java.classexcute.DynamicDispatch();
    Code:
       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   Lnet/kolbe/java/classexcute/DynamicDispatch;

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class net/kolbe/java/classexcute/DynamicDispatch$Man
       3: dup
       4: invokespecial #3                  // Method net/kolbe/java/classexcute/DynamicDispatch$Man."<init>":()V
       7: astore_1
       8: new           #4                  // class net/kolbe/java/classexcute/DynamicDispatch$Woman
      11: dup
      12: invokespecial #5                  // Method net/kolbe/java/classexcute/DynamicDispatch$Woman."<init>":()V
      15: astore_2
      16: aload_1
      17: invokevirtual #6                  // Method net/kolbe/java/classexcute/DynamicDispatch$Human.sayHello:()V
      20: aload_2
      21: invokevirtual #6                  // Method net/kolbe/java/classexcute/DynamicDispatch$Human.sayHello:()V
      24: new           #4                  // class net/kolbe/java/classexcute/DynamicDispatch$Woman
      27: dup
      28: invokespecial #5                  // Method net/kolbe/java/classexcute/DynamicDispatch$Woman."<init>":()V
      31: astore_1
      32: aload_1
      33: invokevirtual #6                  // Method net/kolbe/java/classexcute/DynamicDispatch$Human.sayHello:()V
      36: return
    LineNumberTable:
      line 21: 0
      line 22: 8
      line 23: 16
      line 24: 20
      line 25: 24
      line 26: 32
      line 27: 36
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      37     0  args   [Ljava/lang/String;
          8      29     1   man   Lnet/kolbe/java/classexcute/DynamicDispatch$Human;
         16      21     2 woman   Lnet/kolbe/java/classexcute/DynamicDispatch$Human;
}

  • 0~15行:为man和woman建立内存空间、调用man和woman实例构造器、将实例引用放入局部变量表中
  • 16和20行:将man和woman的对象引用压入栈顶
  • 17和21行:调用虚方法,invokevirtual指令运行解析过程如下:
    1)找到操作数栈顶第一个元素指向的实际类型(C)
    2)在C中找到与常量描述符相符的方法并进行访问权限校验,通过则返回方法直接引用,不通过则返回异常
    3)按照继承关系找C的父类,然后继续执行第二步的搜索和验证
    4)如果都找不到,则抛AbstractMethodError异常

在invokevirtual指令的第一步就确定了接收者的实际类型,并将方法符号引用解析到直接引用,这种运行期根据实际类型确定方法的执行版本分派过程称为动态分派。

3 方法执行

Java编译器输出的指令流是基于栈的指令集架构,它依赖操作数栈进行工作。基于栈的指令集优点在于可移值,缺点在于执行速度稍慢。

SimpleCalculate.java

public class SimpleCalculate {

    public int calc() {
        int a = 100;
        int b = 200;
        int c = 300;
        return (a + b) * c;
    }

}
$ javac SimpleCalculate.java
$ javap -c -l SimpleCalculate.class
public class net.kolbe.java.classexcute.SimpleCalculate {
  public net.kolbe.java.classexcute.SimpleCalculate();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 3: 0

  public int calc();
    Code:
       0: bipush        100
       2: istore_1
       3: sipush        200
       6: istore_2
       7: sipush        300
      10: istore_3
      11: iload_1
      12: iload_2
      13: iadd
      14: iload_3
      15: imul
      16: ireturn
    LineNumberTable:
      line 6: 0
      line 7: 3
      line 8: 7
      line 9: 11
}

image.png

image.png

image.png

image.png

image.png

image.png

image.png

4 常用指令

4.1 常量入栈指令

常量入栈指令的功能是将常数压入操作数栈,根据数据类型和入栈内容的不同,可以分为const系列、push系列和ldc指令

4.1.1 const

指令格式:数据类型 + const + 序号
数据类型:

  • a(aconst):代表对象引用
  • i(iconst):代表整数
  • l(lconst):代表长整数;
  • f(fconst):代表浮点数;
  • d(dconst):代表双精度浮点数;

4.1.2 push

指令格式:数据类型 + push
数据类型:

  • bi(bipush):代表8位整数
  • si(sipush):代表16位整数

4.1.3 ldc

指令格式:ldc + 数据类型
数据类型:

  • 默认ldc:接收8位参数索引值,指向常量池中的int、float、String类型索引
  • ldc2:接收long或double类型

4.2 局部变量压栈指令

局部变量压栈指令将给定的局部变量表中的数据压入操作数栈

4.2.1 load

将局部变量压栈指令可分为三类:

  • xload:通过指定参数把局部变量压入操作数栈,局部变量数量可能超过4个
  • xload_n:表示将第n个局部变量压入操作数栈
  • xaload:表示将数组压入栈

其中x代表数据类型,数据类型分类如下:

  • i:整数
  • l:长整数
  • f:浮点数
  • d:双精度浮点数
  • a:对象索引
  • b:byte
  • c:char
  • s:short

4.3 出栈装入局部变量表指令

出栈装入局部变量表指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值

4.3.1 store

将操作数栈装入局部变量表可分为三类

  • xstore
  • xstore_n
  • xastore

用法与局部变量压栈一样

4.4 通用型指令

4.4.1 dup

指令dup意为duplicate,它会将栈顶元素复制一份并再次压入栈顶,这样栈顶就有两份一模一样的元素

4.4.2 pop

指令pop则把一个元素从栈顶弹出,并直接废弃

4.5 类型转换指令

指令格式:原类型 + 2 + 目标类型
类型字符:

  • i:int
  • l:long
  • f:float
  • d:double
  • b:byte
  • c:char
  • s:short

例如:

public void convertData(int i) {
  // 对应的字节码指令:i2l
  long l = i;
}

4.6 运算指令

4.6.1 基本运行指令

加法指令有:iadd、ladd、fadd、dadd
减法指令有:isub、lsub、fsub、dsub
乘法指令有:imul、lmul、fmul、dmul
除法指令有:idiv、ldiv、fdiv、ddiv
取余指令有:irem、lrem、frem、drem
数值取反有:ineg、lneg、fneg、dneg
自增指令:iinc

4.6.2 位运算指令

位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
按位或指令:ior、lor。
按位与指令:iand、land。
按位异或指令:ixor、lxor。

4.7 对象操作指令

4.7.1 new

指令new用于创建普通对象。它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈

4.7.2 newarray

指令newarray用于创建基本类型的数组

4.7.3 anewarray

指令anewarray用于创建对象数组

4.7.4 multianewarray

指令multianewarray用于创建多维数组

4.7.5 getfield

获取实例对象字段值

4.7.6 putfield

设置实例对象字段值

4.7.7 getstatic

获取类静态字段值

4.7.8 putstatic

设置类静态字段值

4.8 函数调用指令

4.8.1 invokevirtual

虚函数调用,调用对象的实例方法,根据对象的实际类型进行派发,支持多态,也是最常见的Java函数调用方式。

4.8.2 invokeinterface

指接口方法的调用,当被调用对象声明为接口时,使用该指令调用接口的方法

4.8.3 invokespecial

调用一些特殊的函数,比如构造函数、类的私有方法、父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发

4.8.4 invokestatic

用类的静态方法,这个也是静态绑定的

4.8.5 invokedynamic

调用动态绑定的方法,这个是JDK 1.7后新加入的指令

4.9 返回指令

4.9.1 return

函数调用结束前,需要进行返回。返回时,需要使用xreturn指令将返回值存入调用者的操作数栈中。
指令格式:数据类型 + return
数据类型:

  • int:ireturn
  • void:return