字节码执行引擎

运行时栈帧结构

用于支持虚拟机进行方法调用和方法执行背后的数据结构

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息

每个栈帧的大小在编译期就已经确定(根据局部变量表 操作数栈等)

局部变量表

用于存放方法参数和方法内部定义的局部变量,方法的Code属性max_locals数据项确定了局部变量表的最大容量

规范说到每个变量槽都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据

虚拟机实现至少都应当能通过reference做两件事:

对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间

局部变量表中的变量槽是可以重用的,但是这种重用会影响到垃圾回收,如果垃圾收集器发现变量表还存在某个变量的引用 就不会轻易回收它:

byte[] bytes = new byte[1024 * 1024 * 128];
bytes = null; // 不加上这行 bytes不会在gc被调用时被回收
System.gc();

操作数栈

操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中

用来支持各种指令操作

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用

方法返回地址

方法执行后 有两种方式退出方法:

方法退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中

附加信息

《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息

方法调用

方法调用是最普遍、最频繁的操作之一

方法调用的指令:

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本, 这些方法被称为非虚方法

public static void say(){
    System.out.println("hello world");
}
public static void main(String[] args) {
    say();
}
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #21                 // Method say:()V
         3: return
      LineNumberTable:
        line 12: 0
        line 13: 3

分派

静态类型:编译时能确定的类型

所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载

编译过程中,编译器会暂时用符号引用来表示目标方法,这些符号引用存在类常量池中

static void f(Object obj){
    System.out.println("obj");
}
static void f(CharSequence seq){
    System.out.println("seq");
}
static void f(String str){
    System.out.println("str");
}
public static void main(String[] args) {
    Object obj = "str";
    CharSequence seq = "str";
    String str = "str";
    f(obj); // obj
    f(seq); // seq
    f(str); // str
}

动态分派

方法覆写的话会在运行时动态决定应该调用哪个方法

invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,只有方法才会参与多态 字段没有invokevirtual相关指令 所以字段不会多态

单分派与多分派

虚拟机动态分派的实现

为了性能 JVM不会频繁搜索类元数据 而是使用了一个虚方法表:

屏幕截图 2020-10-29 145045

动态类型语言支持

动态类型语言:类型检查的主体过程是在运行期而不是编译期进行的

虚方法调用对性能的会影响 JVM 使用内联缓存加速动态绑定

java.lang.invoke

模拟invokedynamic:

                                              // 返回值类型  参数类型
MethodType methodType = MethodType.methodType(void.class, String.class);
Object obj = System.out;
MethodHandle methodHandle = MethodHandles.lookup()
        .findVirtual(obj.getClass(), "println", methodType).bindTo(obj);
methodHandle.invoke("hello world");

MethodHandle是在模拟字节码层次的方法调用 反射API只能为Java服务 使用invoke包可以用来开发动态语言

实战 自己控制方法调用

static class GrandFather {
    void thinking() {
        System.out.println("i am grandfather");
    }
}
static class Father extends GrandFather {
    void thinking() {
        System.out.println("i am father");
    }
}

static class Son extends Father {
    void thinking() {
        // 在这里填入适当的代码(不能修改其他地方的代码)
        // 实现调用祖父类的thinking()方法,打印"i am grandfather"
        try {
            MethodType mt = MethodType.methodType(void.class);
            Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
           lookupImpl.setAccessible(true);
           MethodHandle mh = ((MethodHandles.Lookup) lookupImpl.get(null)).findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
                try {
                    mh.invoke(this);
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

执行引擎

早期的JVM是通过解释字节码来执行的 但后面也出现了编译为本地机器码的编译器 所以现代的Java是拥有编译模式以及解释模式亦或者混合模式

对于JVM来说 基于栈移植容易 实现容易

栈架构指令集的缺点在于速度稍慢

实战:分析一段简单的四则运算

public int calc(){
    int a = 100;
    int b = 200;
    int c = 300;
    return (a + b) * c;
}
public int calc();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         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 9: 0
        line 10: 3
        line 11: 7
        line 12: 11

javap提示这段代码需要深度为2的操作数栈和4个变量槽的局部变量空间

1.Bipush指令的作用是将单字节的整型常量值(-128~127)推入操作数栈顶 参数为100

屏幕截图 2020-10-29 153202

2.istore_1指令的作用是将操作数栈顶的整型值出栈并存放到第1个局部变量槽中 后面的istore 以及xxpush都是一样

屏幕截图 2020-10-29 153409

3.iload_1指令的作用是将局部变量表第1个变量槽中的整型值复制到操作数栈顶

屏幕截图 2020-10-29 153448

4.load_2指令的执行过程与iload_1类似,把第2个变量槽的整型值入栈

屏幕截图 2020-10-29 153545

5.iadd指令的作用是将操作数栈中头两个栈顶元素出栈,做整型加法,然后把结果重新入栈

屏幕截图 2020-10-29 153628

6.iload_3指令把存放在第3个局部变量槽中的300入栈到操作数栈中

屏幕截图 2020-10-29 153701

7.指令imul是将操作数栈中头两个栈顶元素出栈,做整型乘法,然后 把结果重新入栈,与iadd完全类似

8.ireturn指令是方法返回指令之一,它将结束方法执行并将操作数栈顶的整型值返回给该方法的调用者

屏幕截图 2020-10-29 153756