物理机的执行引擎是直接建立在处理器,缓存,指令集和操作系统上的,而虚拟机的执行引擎是由软件实现的。
1. 运行时栈帧结构
1.1 简介
Java虚拟机是以方法作为最基本的执行单位。“栈帧”则是用来支持虚拟机进行方法调用和执行背后的数据结构。 每一个方法从调用开始到执行结束,都对应着一个栈帧在虚拟机栈里的从入栈到出栈的过程。
1.2 大小与结构
在java程序编译成Class文件时,栈帧需要多大的局部变量表。多深的操作数栈就已经被分析出来了,并写入了方法表的Code属性当中。
同一时刻,同一条线程里,在调用堆栈的所有方法都处于执行状态,而对执行引擎来说,在活动线程里,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,被称为“当前栈帧”。
1.3 局部变量表
这是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。
以变量槽为最小单位,一个变量槽可以放一个32位以内的数据结构,有boolean, int, byte, char, short, float,reference和returnAddress。
Java虚拟机通过索引的方式使用局部变量表,从0开始。如果访问的是double和long这种64位的,就会同时用第N和N+1两个变量槽。不允许单独访问其中一个,类加载的验证阶段字节码检验会抛出异常。
当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。
如果执行的是实例方法(无static),那第零位索引默认是用于传递方法所属实例对象的引用,在方法中可以用“this”来访问到这个隐含的参数。
为节省栈帧消耗的内存空间,变量槽是可以重用的,但会有问题出现。
代码离开了placeholder的作用域,但之后并没有发生过任何对局部变量表的读写操作,placeholder原本占用的变量槽还没有被其他变量复用,GC不掉
1.4 操作数栈
当一个方法刚刚开始执行时,这个方法的操作数栈是空的,在执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。比如,在做算术运算时,通过将运算涉及的操作数栈压入栈顶,然后调用运算指令进行的,又比如在调用其他方法时是通过操作数栈来进行方法参数的传递。举例:整数相加的字节码指令isadd,这条指令在运行时要求操作数栈中最接近栈顶的两个元素已经存入了两个int数值,当执行这条指令时,会把这两个int值出栈并相加,再把结果压入栈中。
两个不同栈帧作为不同方法的虚拟机栈的元素,是完全独立的。但目前设计上,会出现部分重叠,进行方法调用时可以直接共用一部分数据。
1.5 动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。
Class文件的常量池中的符号引用,一部分在类加载时或第一次使用的时候就转为直接引用,称为静态解析。而另一部分将在每一次运行期间都转为直接饮用,称为动态连接(具体过程在2. 方法调用)。
1.6 方法返回地址
方法开始执行后,有两种方法退出这个方法。1. 执行引擎遇到任意一个方法返回的字节码指令。 2. 遇到异常。
无论哪种退出方式,在方法退出后,都要返回到最初方法被调用的位置。一般来说,方法正常退出,主调方法的PC计数器的值就可以作为返回地址,异常时需要异常处理表。
方法退出的具体过程(当前栈帧出栈):
恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈,调整PC计数器的值以指向方法调用指令后面的一条指令等。
2. 方法调用
方法调用阶段唯一任务是确定被调用的方法的版本。
2.1 解析
所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,化为直接引用。
调用不同类型的方法,Java有五种字节码指令:
- invokestatic :调用静态方法
- invokespecial。调用实例构造器
<init>()
⽅法、私有方法和父类中的⽅法。 - invokevirtual。用于调用所有的虚⽅法。
- invokeinterface。调用接口方法,会在运⾏时再确定⼀个实现该接口的对象。
- invokedynamic。先在运⾏时动态解析出调用点限定符所引用的⽅法,然后再执⾏该方法。
前⾯4条调用指令,分派逻辑都固化在Java虚拟机内部,⽽invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯⼀的调用版本, Java语⾔⾥符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final 修饰的方法(尽管它使⽤invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。 这些方法统称为“⾮虚方法”,与之相反,其他⽅法就被称为“虚方法”。
解析引用是一个静态的过程,编译期间完全确定,运行期不可变。
2.2 分派
(P415 代码详细解释)
面向对象的三个基本特征:继承,封装和多态。
重载(相同名词,不同参数列表)和重写就是多态的体现。
2.2.1 静态分派
静态分派的最典型应用表现就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,这点也是为何一些资料选择把它归入“解析”而不是“分派”的原因。
虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为
判定依据的。由于静态类型在编译期可知,所以在编译阶段,Javac编译器就根据参数的静态类型决定了会使用哪个重载版本,因此选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。
2.2.2 动态分派
动态分派跟方法重写有密切关系。
2.2.3 单分派和多分派
方法的接收者和参方法的参数称为“宗量”。单分派是根据一个宗量对目标方法进行选择。