虚拟机字节码执行引擎

执行引擎概述

执行引擎是虚拟机最核心的组成部分之一。可能分为:

  • 解释执行:通过解释器执行
  • 编译执行:通过即时编译器产生本地代码执行。

    运行时栈帧结构

    栈帧

    用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素。
    注意:每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。


    栈帧存储的东西包括:

    方法的局部变量表

  • 是一组变量值存储空间,用于存放参数和方法内部定义的局部变量。
  • 在编译为Class文件时,就在方法的Code属性的max_locals确定了局部变量表的最大容量。
  • 在方法执行时,虚拟机使用局部变量表来完成参数值到参数变量列表的传递过程。
  • 如果是非静态方法,局部变量表中第0个索引的slot默认是用于传递方法所属的对象实例的引用,即this关键字。其余参数按照参数表顺序排列。
  • 为了节省空间,slot可重用。
    slot(Variable Slot,变量槽)
  • 局部变量表容量的最小单位。
  • 每个slot都应该可以存放一个boolean、byte、char、short、int、float、reference、returnAddress类型的数据。
  • 一个slot可以存放32位以内的数据类型。
  • reference表示对对象数据的引用:
    • 可以通过此引用直接或间接地查找到对象在Java堆中的数据存放的起始地址索引。
    • 可以通过此引用直接或间接地查找到对象所属数据类型在方法区中的存储类型信息。
  • 64位的数据类型,虚拟机会以最高位对齐的方式为其分配两个连续的slot空间。
  • 64位的只有long和double两种。
    虚拟机通过索引定位的方式使用局部变量表。索引范围从0开始,到最大的slot数量。

    注:局部变量必须赋初值,与类变量不一样。

    操作数栈

  • 也就是操作栈,后进先出(LIFO)栈。
  • 也是在编译时就已经确定栈的最大深度,在方法的Code属性的max_stacks确定。
  • 操作数栈的每一个元素可以是任意的数据类型,包括long和double。
  • 32位所占的栈容量为1,64位所占的栈容量为2。
  • 在方法执行的任何时候,操作数栈的深度都不会超过max_stacks。

    动态链接

    方法返回地址

    在方法退出之后,都需要返回到方法被调用的位置程序才能继续执行。方法退出的过程,实际上就等于把当前栈出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向调用指令后面的一条指令。

    方法的退出方式:
  • 正常完成出口:调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器的值。
  • 异常完成出口:返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

    额外的附加信息

    方法调用

  • 方法调用不同于方法执行。

    解析

    分派

    静态分派(Method Overload Resolution,方法重载分派)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 方法静态分派演示
    */
    public class MethodOverloadResolution {
    static abstract class Human{}
    static class Man extends Human{}
    static class Woman extends Human{}
    public void sayHello(Human human){
    System.out.println("hello human!");
    }public void sayHello(Man man){
    System.out.println("hello man!");
    }public void sayHello(Woman woman){
    System.out.println("hello woman!");
    }

    public static void main(String[] args) {
    MethodOverloadResolution m = new MethodOverloadResolution();
    Human man = new Man();
    Human woman = new Woman();
    m.sayHello(man);
    m.sayHello(woman);
    }
    }

运行结果:

1
2
3
4
hello human!
hello human!

Process finished with exit code 0

分析
首先解释两个概念:Human称为man这个对象的静态类型(Static Type),而Man称为man这个对象的实际类型(Actual Type)

  • 静态类型的变化仅仅在使用时发生,变量本身的静态类型不会改变,静态类型在编译期可知
  • 实际类型变化是在运行期才确定,编译器在编译的时候并不知道一个对象的实际类型是什么。
  • 例:
    1
    2
    3
    4
    5
    6
    //实际类型变化
    Human man = new Man();
    Human woman = new Woman();
    //静态类型变化
    m.sayHello((Man)man);
    m.sayHello((Woman)woman);

继续解读以上代码:在main()方法的两次sayHello()方法,已经确定了对象m是方法的接收者。在这个前提下,要使用哪个方法进行重载,完全取决于参数的参数列表(包括参数类型、参数个数等)。代码中刻意地定义了两个静态类型相同而实际类型不同的变量,但是编译器在重载的时候是通过参数的静态类型而不是实际类型进行判断的。并且静态类型是编译期可知。

注:方法重载是静态分派的典型应用。

动态分派

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.laowang.vm.invoke;

/**
* 方法动态分派演示
*/
public class MethodOverrideResolution {
static abstract class Human{
protected abstract void sayHell();
}
static class Man extends Human{
@Override
protected void sayHell() {
System.out.println("man say hello");
}
}static class Woman extends Human{
@Override
protected void sayHell() {
System.out.println("woman say hello");
}
}

public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHell();
woman.sayHell();
man = new Woman();
man.sayHell();
}
}

运行结果:

1
2
3
4
5
man say hello
woman say hello
woman say hello

Process finished with exit code 0

注:方法重写是动态分派的典型应用。

单分派与多分派

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 单分派和多分派演示
*/
public class Dispatcher {
static class QQ{
}
static class _360{
}
public static class Father{
public void hardChoice(QQ arg){
System.out.println("father choose qq!");
}
public void hardChoice(_360 arg){
System.out.println("father choose 360!");
}

} public static class Son extends Father{
@Override
public void hardChoice(QQ arg){
System.out.println("son choose qq!");
}
@Override
public void hardChoice(_360 arg){
System.out.println("son choose 360!");
}

public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
}

运行结果:

1
2
father choose 360!
son choose qq!

动态语言支持

动态类型语言

动态类型语言的关键特征是它的类检查的主体过程是在运行期而不是编译期。

注:变量无类型而变量值才有类型是动态类型语言的一个重要特征。

JDK1.7与动态类型

java.lang.invoke包

在以前单纯依靠符号引用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法的机制,称为MethodHandle
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.laowang.vm;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

import static java.lang.invoke.MethodHandles.lookup;

/**
* Method handle 基础用法演示
*/
public class TestMethodHandle {
static class ClassA {
public void println(String s) {
System.out.println(s);
}
}

public static void main(String[] args) throws Throwable {
Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
getPrintlnMH(obj).invoke("icyfenix");
}

/**
* MethodType:代表“方法类型”,包含了方法的返回值和具体参数(第一个参数和第二个及以后的参数)
* lookup()方法来自于MethodHandle.lookup,这句的作用是在指定类中查找符合给定的方法名称,方法类型,并且符合调用权限的方法句柄。
*
* 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接收者,也就是this指向的对象,
* 这个参数以前是放在列表中进行传递的,而现在提供了bindTo()方法来完成这件事情。
* @param receiver
* @return
* @throws Throwable
*/
private static MethodHandle getPrintlnMH(Object receiver) throws Throwable{
MethodType mt = MethodType.methodType(void.class,String.class);
return lookup().findVirtual(receiver.getClass(),"println",mt).bindTo(receiver);
}
}

但是,不是可以用反射(Reflection)来解决这样的问题吗?

MethodHandle与反射的区别
  • 反射是在Java代码级别的模拟方法调用,而MethodHandle是在字节码级别。
  • MethodHandle的findStatic(),findVirtual(),findSpecial()对应于invokestatic,invokevirtual&invokeinterface和invokespecial这几条字节码指令执行的权限校验行为,但是在Reflection API中无需关心。
  • Reflection中的java.lang.reflect.Hethod对象所包含的信息比java.lang.invoke.MethodHandle对象所包含的信息多。
  • **最关键的一点,Reflection API只是为了Java语言服务,而MethodHandle可以设计成服务于所有Java虚拟机,当然也包括Java语言。**

    invokedynamic指令

    每一处含有invokedynamic指令的位置都称作”动态调用点(Dynamic Call Site)“,这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是CONSTANT_MethodrefDynamic_info常量,从这个常量中可以得到三个信息:
  • Bootsrap method:引导方法,有固定的参数,返回值为java.lang.invoke.CallSite,这个代表真正的要执行的目标方法调用。
  • Method Type:方法类型
  • 名称

    掌握方法分派规则*