字节码执行引擎

字节码的执行

  1. 解释执行
  2. 编译执行
  3. JIT编译与解释混合执行
flowchart TD    A[方法调用] --> B{已编译?}    B -->|是| C[执行编译后的机器码]    B -->|否| D[方法调用计数器加1]    D --> E{计数是否超过阈值?}    E -->|否| F[解释方式执行]    E -->|是| H[提交编译请求]    H --> I[编译器]    I --> J[后台执行编译]    J --> K[Code Cache]    K --> C    C --> 方法返回    F --> 方法返回

这样就造成机器在热机所承载的负载可能会比冷机的高

热点代码检测

还有一种新的编译方式,即所谓的 AOT(Ahead-of-Time Compilation),直接将字节码编译成机器代码,这是 graalvm 所做的

运行时栈帧结构

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

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

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

局部变量表

用于存放方法参数和方法内部定义的局部变量,方法的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