第八章 虚拟机字节码执行引擎
[目录]
概述
1 概述
- 不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(
通过解释器执行
)和编译执行(通过即时编译器产生本地代码执行
)两种选择也可能两者兼备 - 从外观上看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是
字节码文件
,处理过程是字节码解析的等效过程
,输出的是执行结果
- 本章将主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行。
2 运行时栈帧结构
-
栈帧(Stack Frame)是虚拟机运行时数据区中的
虚拟机栈(Virtual Machine Stack)的栈元素
,用于支持虚拟机进行方法调用和方法执行的数据结构 -
存储了方法的
局部变量表
、操作数栈
、动态连接
和方法返回地址
等信息 -
每一个方法从调用开始至执行完成的过程
**---------> **一个栈帧在虚拟机栈里面从入栈到出栈的过程
-
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中
-
一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态(方法间互相调用)
-
对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为
当前栈帧(Current StackFrame)
,与这个栈帧相关联的方法称为当前方法(Current Method)
。
2.1 局部变量表
- 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放
方法参数和方法内部定义的局部变量
。在Java程序编译为Class文件时,就在方法的Code属性
的max_locals
数据项中确定了该方法所需要分配的局部变量表的最大容量。 - 局部变量表的容量以
变量槽(Variable Slot,下称Slot)
为最小单位 - 一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有
boolean
、byte
、char
、short
、int
、float
、reference
和returnAddress
种类型 -
reference类型
表示对一个对象实例的引用,一是从此引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引
,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息
2.2 操作数栈
-
操作数栈(Operand Stack)
也常称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度
也在编译的时候写入到Code属性
的max_stacks数据项
中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。
2.3 动态连接
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的
动态连接(Dynamic Linking)
。
2.4 方法返回地址
两种返回的方法:
正常完成出口(Normal Method Invocation Completion)
和异常完成出口(Abrupt Method Invocation Completion)
无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息
3 方法调用
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。
3.1 解析
- 调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为
解析(Resolution)
-
第七章讲过:目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这
种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的 - 符合“编译期可知,运行期不可变”这个要求的方法,主要包括
静态方法
和私有方法
两大类
Java虚拟机里面提供了5条方法调用字节码指令
方法名 | 解释 | 备注 |
---|---|---|
invokestatic | 调用静态方法 | 无 |
invokespecial | 调用实例构造器<init>方法、私有方法和父类方法 | 无 |
invokevirtual | 调用所有的虚方法 | 无 |
invokeinterface | 调用接口方法,会在运行时再确定一个实现此接口的对象 | 无 |
invokedynamic | todo | todo |
只要能被
invokestatic
和invokespecial
指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法
、私有方法
、实例构造器
、父类方法
4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用
被
invokestatic
和invokespecial
指令调用的方法 称为非虚方法
,与之相
反,其他方法称为虚方法
(除去final方法,后文会提到)
3.2 分派
3.2.1 静态分派
- 定义: 依赖静态类型来定位方法执行版本的分派动作
- 静态分派发生在
编译阶段
- 确定静态分派的动作实际上不是由虚拟机来执行的。
示例代码
package study8;
/**
* Created by haicheng.lhc on 05/04/2017.
*
* @author haicheng.lhc
* @date 2017/04/05
*/
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 sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
结果分析:
我们把上面代码中的
“Human”
称为变量的静态类型(Static Type)
,或者叫做的外观类型(Apparent Type),后面的“Man”
则称为变量的实际类型(Actual Type)
,静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么.虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的
3.2.2.动态分派
- (Override)有着很密切的关联
示例代码
package study8;
/**
* Created by haicheng.lhc on 05/04/2017.
*
* @author haicheng.lhc
* @date 2017/04/05
*/
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();
}
}
解析
使用javap分析
Compiled from "DynamicDispatch.java"
public class study8.DynamicDispatch extends java.lang.Object
SourceFile: "DynamicDispatch.java"
InnerClass:
#9= #4 of #7; //Woman=class study8/DynamicDispatch$Woman of class study8/DynamicDispatch
#11= #2 of #7; //Man=class study8/DynamicDispatch$Man of class study8/DynamicDispatch
abstract #13= #12 of #7; //Human=class study8/DynamicDispatch$Human of class study8/DynamicDispatch
minor version: 0
major version: 50
Constant pool:
const #1 = Method #8.#22; // java/lang/Object."<init>":()V
const #2 = class #23; // study8/DynamicDispatch$Man
const #3 = Method #2.#22; // study8/DynamicDispatch$Man."<init>":()V
const #4 = class #24; // study8/DynamicDispatch$Woman
const #5 = Method #4.#22; // study8/DynamicDispatch$Woman."<init>":()V
const #6 = Method #12.#25; // study8/DynamicDispatch$Human.sayHello:()V
const #7 = class #26; // study8/DynamicDispatch
const #8 = class #27; // java/lang/Object
const #9 = Asciz Woman;
const #10 = Asciz InnerClasses;
const #11 = Asciz Man;
const #12 = class #28; // study8/DynamicDispatch$Human
const #13 = Asciz Human;
const #14 = Asciz <init>;
const #15 = Asciz ()V;
const #16 = Asciz Code;
const #17 = Asciz LineNumberTable;
const #18 = Asciz main;
const #19 = Asciz ([Ljava/lang/String;)V;
const #20 = Asciz SourceFile;
const #21 = Asciz DynamicDispatch.java;
const #22 = NameAndType #14:#15;// "<init>":()V
const #23 = Asciz study8/DynamicDispatch$Man;
const #24 = Asciz study8/DynamicDispatch$Woman;
const #25 = NameAndType #29:#15;// sayHello:()V
const #26 = Asciz study8/DynamicDispatch;
const #27 = Asciz java/lang/Object;
const #28 = Asciz study8/DynamicDispatch$Human;
const #29 = Asciz sayHello;
{
public study8.DynamicDispatch();
Code:
Stack=1, Locals=1, Args_size=1
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 9: 0
line 22: 4
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=3, Args_size=1
0: new #2; //class study8/DynamicDispatch$Man
3: dup
4: invokespecial #3; //Method study8/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4; //class study8/DynamicDispatch$Woman
11: dup
12: invokespecial #5; //Method study8/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6; //Method study8/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6; //Method study8/DynamicDispatch$Human.sayHello:()V
24: new #4; //class study8/DynamicDispatch$Woman
27: dup
28: invokespecial #5; //Method study8/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6; //Method study8/DynamicDispatch$Human.sayHello:()V
36: return
LineNumberTable:
line 30: 0
line 31: 8
line 32: 16
line 33: 20
line 34: 24
line 35: 32
line 36: 36
}
虽然17 21 语句完全一样,但是结果却不一样,原因是:从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:
1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
3.2.3单分派与多分派
- 方法的接收者与方法的参数统称为方法的宗量,这个定义最早应该来源于《Java与模式》一书。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种
- 以Java语言的静态分派属于多分派类型
- Java语言的动态分派属于单分派类型
3.2.4 虚拟机动态分派的实现
-
虚方法表(Vritual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Inteface Method Table,简称itable)
,使用虚方法表索引来代替元数据查找以提高性能。
3.3 动态类型语言支持
4 基于栈的字节码解释执行引擎
- 虚拟机是如何调用方法的内容已经讲解完毕,从本节开始,我们来探讨虚拟机是如何执行方法中的字节码指令的
- 整个运算过程的中间变量都以操作数栈的出栈、入栈为信息交换途径