##1. 运行时栈帧结构 栈帧
是虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(VM Stack)的栈元素。每个栈帧都包括了:局部变量表
、操做数栈
、动态链接
、方法返回地址和一些额外的附加信息
。每个方法从调用开始到执行结束的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈
的过程。 在编译程序代码的时候,栈帧中须要多大的局部变量表、多深的操做数栈都已经彻底肯定,并写入到方法表的Code属性之中。所以一个栈帧须要分配多大的内存不会受到运行期变量数据的影响。java
###1.1 局部变量表 局部变量表(Local Variable Table)是一组变量值
的存储空间,用于存放方法参数
和方法中的局部变量的值
。 局部变量中的容量以容量槽(Variable Slot)为最小单位。虚拟机经过索引定位
的方式使用局部变量表,索引值的范围从0开始至局部变量表最大的Slot容量
。 在方法执行时,虚拟机是经过局部变量表完成参数值到参数变量列表的传递过程的,若是执行的实例方法,那局部变量表中第0位索引的Slot默认用于传递方法所属对象实例的引用,而后按顺序存储存储方法的参数,最后根据方法体内定义的变量顺序和做用域分配其他的Slot。数据结构
###1.2 操做数栈 操做数栈(Oprand Stack)是一个先进先出的栈。跟局部变量表同样,操做数栈的深度在编译的时候已写入到Class文件中Code属性的max_stacks数据项中。通常32位数据类型占用一个Slot容量,64位数据类型占用两个Slot容量。ide
###1.3 动态链接 每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接。性能
###1.4 方法返回地址 当一个方法开始执行后,只有两种方式能够退出这个方法。第一种:执行引擎遇到方法的返回指令,这种退出方法的方式称为正常完成出口
;第二种:方法在执行过程当中遇到异常,而且这个方法没有在方法体内获得处理,不管是Java虚拟机内部产生的异常,仍是代码中经过athrow字节码指令产生的异常, 只要在本方法的异常表中没有搜索到匹配的异常处理器,就会致使方法退出,这种方式称为异常完成出口
。不管采用何种方式退出,在退出方法以后,都要返回到方法的调用位置。通常来讲,方法正常退出时,调用者的PC计数器的值能够做为返回地址,栈帧中极可能会保存这个计数器的值。而方法异常退出时,返回地址是经过异常异常处理器表来肯定的。code
##2. 方法调用 方法调用并不等同于方法执行,方法调用阶段惟一的任务
就是肯定被调用方法的版本(即调用哪一个方法),暂时不涉及方法内部的具体运行过程。对象
###2.1 解析 全部方法调用中的目标方法在Class文件里都是一个常量池的符号引用,在类加载的解析阶段
,会将其中一部分符号引用转化为直接引用,这种转化成立的前提是:方法在程序真正运行以前就有一个肯定的调用版本,而且这个方法的调用版本在运行期是不可变的
。这类方法的调用成为解析(Resolution)
。 在Java语言中符合编译器可知,运行期不可变
这个要求的方法,主要包括:静态方法
和私有方法
。这两个方法都适合在类加载阶段进行解析。 在Java虚拟机中提供了5中方法调用字节码指令:继承
<init>
方法、私有方法和父类方法只要能被invokestatic和invokespecial指令调用的方法,均可以在解析阶段中肯定肯定惟一的调用版本,符合这个条件的方法有:静态方法
、私有方法
、实例构造器
、弗雷方法
4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法能够称为非虚方法
,与之相反,其余方法称为虚方法
。 虽然被final修饰的方法是使用invokevirtual指令来调用的,可是因为它没法被覆盖,没有其余版本,因此final方法是虚方法
。索引
###2.1 分派接口
####2.1.1 静态分派内存
Human man = new Man();
上面代码中的“Human”称为变量的静态类型
,或者成为外观类型
,后面的Man
则称为变量的实际类型。静态类型的变化仅仅在使用时发生,而变量自己的静态类型不会被改变,最终的静态类型是在编译器可知的,而实际类型变化的结果在运行期才可肯定。虚拟机编译器(准确来讲是)在重载时是经过静态类型而不是实际类型做为判断依据的
。 全部依赖静态类型来定位方法执行版本的分派动做成为静态分派
。静态分派的典型应用是方法重载。
###2.1.2 动态分派 在运行期根据根据实际类型来肯定方法执行版本的分派过程成为动态分派
。动态分派的主要体现是:重写(Override)。invokevirtual指令的运行时解析过程分为一下步骤:
###2.1.3 单分派与多分派 方法的接受者与方法的参数统称为方法的宗量
。单分派是根据一个宗量对目标方法进行选择。多分派是根据多于一个宗量对目标方法进行选择。 编译阶段编译器的选择过程(即静态分派过程),Java语言的静态分派属于多分派。
###2.1.4 虚拟机动态分派的实现 动态分派的方法选择过程须要在运行时类的方法元数据中搜索合适的目标方法,基于性能的考虑,一般在类方法区中创建一个虚方法表(Virtual Method Table,与此对应,在invokeinterface执行时也会用到接口方法表-Interface Method Table),使用虚方法表索引来代替元数据查找以提升性能。 虚方法表中存放着方法的实际入口地址,若是某个方法在子类没有被重写,那子类的虚方法表的地址入口与父类相同方法的地址入口是一致的,都指向父类的实际入口;若是子类重写了这个方法,那子类虚方法表中的地址将会替换为指向子类实现版本的入口地址。 虚方法表通常在类加载的链接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的虚方法表也初始化完毕。
###2.2 动态类型语言支持
####2.2.1 动态类型语言 动态类型语言的关键特性是:它的类型检查的主题过程是在运行期而不是编译期
;在编译期就进行类型检查过程的语言成为静态类型语言
。变量无类型而变量值才有类型
也是动态类型语言的一个重要特征。
####2.2.2 java.lang.invoke包 这个包的主要目的:在以前单纯依靠符号引用来肯定调用的目标方法这种方式以外,提供了一种新的动态肯定目标方法的机制,称为MethodHandle
。 MethodHandle演示:
import static java.lang.invoke.MethodHandles.Lookup; import java.lang.invoke.MethodType; import java.lang.invoke.MethodHandle; public class MethodHandleTest { 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(); //下面这句能够正确调用到println方法 getPrintlnMH(obj).invokeExact("icyfenix"); } private static MethodHandle getPrintlnMH(Object receiver) throws Throwable { //MethodType: 表明“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及之后的参数) MethodType mt = MethodType.methodType(void.class,String.class); //lookup()来自于MethodHandle.Lookup, //这句的意思是:在指定类中查找符合给定的方法名称、方法类型,而且符合调用权限的方法句柄。 //由于这里调用的是一个虚方法,方法的第一个参数应该是隐式的,表明方法的接收者, //此参数之前放在参数列表中进行传递,如今提供了bindTo()方法来完成这件事。 return lookup().findVirtual(receiver.getClass(),"println",mt).bindTo(receiver); } }