8.1概述
执行引擎:输入字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果
8.2运行时栈帧结构
栈帧是虚拟机用于方法调用和方法执行的数据结构,是虚拟机运行时数据区的虚拟机栈的栈元素
栈帧存储了方法的局部变量表、操作数栈、动态连接、方法返回地址等信息
每一个方法从调用开始到执行结束,就是栈帧在虚拟机栈中入栈出栈的过程
在编译期间,栈帧需要多大的局部变量表、多深的操作数栈都已经完全确定,并且写入方法的code 属性中
一个栈帧需要分配多大内存,不会受到程序运行期变量数据影响
在活动线程中,栈顶的栈帧才是有效的,称为当前栈帧,相关联的方法称为当前方法
8.2.1局部变量表
是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量
在Java程序编译为class时,就在方法的code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量
局部变量表的容量以变量槽slot为最小单位
一个slot可以存放一个32位以内的数据,Java中32位以内的数据类型有:boolean、byte、char、short、int、float、reference、returnAddress
通过Reference类型可以:
从此引用直接或者间接的获取到对象在堆上存放的起始地址索引
此引用中直接或者间接的查找到对象所属数据类型在方法区的存储类型信息
对于64位(long,double)的数据类型,虚拟机采用高位对齐的方式为其分配两个slot空间
虚拟机通过索引定位的方式使用局部变量表
方法执行过程中,虚拟机通过局部变量表完成变量值到参数列表的传递过程
对于实例方法,局部变量表中第0位索引的slot默认传递方法所属对象实例的引用
为了节省栈帧空间,slot空间可以重用,但是会有额外的副作用,例如影响垃圾收集
8.2.2操作数栈(操作栈)
是一个后入先出栈
在编译时候写入code属性的max_stacks数据项中
操作数栈的每一个元素可以是任意的Java数据类型
32位数据类型占用的栈容量为1,64位数据类型占用的栈容量为2
操作数栈中元素的数据类型和字节码指令的序列严格匹配
概念模型上,两个栈帧完全相互独立,但大多虚拟机做了优化处理,使两个栈帧出现一部分重叠(方法调用可以共用一部分数据,无需进行额外的参数复制传递)
8.2.3动态连接
每个栈帧都有一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
8.2.4方法返回地址
正常完成出口:PC计数器的值可以作为返回地址,栈帧中很可能保存这个计数值
异常完成出口:通过异常处理器表来确定返回地址,栈帧中一般不会保存这部分信息
方法退出等同于当前栈帧出栈,可能执行的操作有:
恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令
8.2.5附加信息
8.3方法调用
方法调用不等同于方法执行,唯一任务就是确定被调用方法的版本
8.3.1解析
调用目标在程序写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析
编译期可知,运行期不变
静态方法
私有方法
相对应的5条方法调用字节码指令
invokestatic:调用静态方法
invokespecial:调用实例构造器init方法、私有方法、父类方法
invokevirtual:调用所有的虚方法
invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象
invokedynamic:现在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法
只要能被invokestatic和invokespecial指令调用的方法,都可以再解析阶段中确定唯一的调用版本,符合这个条件的由静态方法、构造方法、私有方法、父类方法四大类,这些方法称为非虚方法(还包含final修饰的方法,无法被覆盖,没有其他版本);与之相反的称为虚方法(final修饰除外)
8.3.2分派
静态分派
静态类型(外观类型):变量本身的静态类型不会被改变,最终的静态类型是在编译期可知的
实际类型:变化结果在运行期才确定
使用哪个版本的重载,完全取决于传入参数的数量和数据类型;虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据;并且静态类型是编译期可知的,因此,在编译阶段,javac编译器会根据参数的静态类型决定使用哪个版本的重载
所有依赖静态类型来定位方法执行版本的分派称为静态分派
静态分派的典型应用就是方法重载
动态分派
重写
invokevirtual的运行时解析过程:
找到操作数栈顶的第一个元素指向的对象的实际类型,记作C
如果类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,如果不存在,则返回java.lang.IllegalAccessError异常
否则,按照继承关系从下往上依次对C的各个父类进行第二步的查搜索和验证过程
如果始终没有找到,则抛出java.lang.AbstractMethodError异常
运行期根据实际类型确定方法版本的分派称为动态分派
单分派和多分派
方法的接受者方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派
单分派是根据一个宗量对目标方法进行选择
多分派是根据多于一个宗量对目标方法进行分派
静态分派属于多分派
动态分派属于单分派
虚拟机动态分派的实现
方法表
8.3.3动态类型语言支持
动态类型语言
关键特征是类型检查的主体过程是在运行期而不是编译期
变量无类型而变量值有类型
静态类型语言在编译期提供严谨的类型检查
动态类型语言提供了更大的灵活性
JDK7与动态类型语言
invokedynamic指令以及java.lang.invoke包出现
java.lang.invoke包
提供了一种动态确定目标方法的机制,称为MethodHandle
MethodHandle和反射(Reflection)区别:
本质上将都是在模拟方法调用,但反射模拟Java代码层次的调用,MethodHandle模拟字节码层次的调用
反射中的java.lang.Method对象远比MethodHandle机制的java.lang.MethodHandle对象所彪悍的信息多,反射是重量级,MethodHandle是轻量级
MethodHandle优化
invokedynamic指令
每一处含有invokedynamic指令的地方都称为动态调用点
掌控方法分派规则
8.4基于栈的字节码解释执行引擎
8.4.1解释执行
javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,在遍历语法树生成线性的字节码指令流的过程
一部分在虚拟机之外进行,而解释器是在虚拟机内部,所以Java程序的编译是半独立的实现
8.4.2基于栈的指令集和基于寄存器的指令集
基于栈的指令集主要优点是可移植性、代码更加紧凑、编译实现更简单;缺点是执行速度相对慢点
8.4.3基于栈的解释器执行过程
参考文献:
[1] 深入理解Java虚拟机 第二版 --周志明