一、概述
执行引擎是java虚拟机最核心的组成部件之一。虚拟机的执行引擎由自己实现,所以可以自行定制指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。本节将主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行。
二、运行时栈帧结构
1.什么是栈帧
栈帧也叫过程活动记录,是编译器用来进行方法调用和方法执行的一种数据结构,它是虚拟机运行是数据区域中的虚拟机栈的栈元素。
栈帧包括了局部变量表、操作数栈、动态链接、方法返回地址和额外的一些附加信息。
在编译过程中,局部变量表的大小已经确定,操作数栈深度也已经确定,因此栈帧在运行的过程中需要分配多大的内存是固定的,不受运行时影响。
对于的有逃逸的对象也会在栈上分配内存,对象的大小其实在运行是也是确定的,因此即使出现了栈上分配,也不会改变栈帧大小。
栈帧的结构图如下:
一个线程中,可能调用链会很长,很多方法都同时处于执行状态。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是最有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的字节码指令反对当前栈帧进行操作。
2.局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位。 一个Slot可以存放一个32位以内(boolean、byte、char、short、int、float、reference和returnAddress)的数据类型,reference类型表示一个对象实例的引用,returnAddress已经很少见了,可以忽略。
需要注意的是:
1)局部变量表没有初始值,要使用局部变量,必须先赋值
2)slot复用问题。当一个变量的PC寄存器的值大于slot的作用域的时候,slot是可以复用的,这样的话就会导致JVM难以回收,所以在使用对象完后,要把对象指定为null,这样才会方便垃圾回收。
3.操作数栈
操作数栈(Operand Stack) 也常称为操作栈,它是一个后入先出栈。当一个方法执行开始时,这个方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是 出栈/入栈操作。
在概念模型中,一个活动线程中两个栈帧是相互独立的。但大多数虚拟机实现都会做一些优化处理:让下一个栈帧的部分操作数栈与上一个栈帧的部分局部变量表重叠在一起,这样的好处是方法调用时可以共享一部分数据,而无须进行额外的参数复制传递。
4.动态连接
指向该栈所属的方法。
5.方法返回地址
方法调用时通过一个指向方法的指针指向方法的地址,方法返回时将回归到调用处,那个地方就是返回地址。
6.附加信息
虚拟机规范中允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中。这部分信息完全取决于虚拟机的实现。
三、方法调用
方法调用不等同于方法的执行,方法调用阶段的唯一任务就是确定被调用方法的版本。
方法调用阶段的目的:确定被调用方法的版本(哪一个方法),不涉及方法内部的具体运行过程,在程序运行时,进行方法调用是最普遍、最频繁的操作。一切方法调用在Class文件里存储的都只是符号引用,这是需要在类加载期间或者是运行期间,才能确定为方法在实际 运行时内存布局中的入口地址(相当于之前说的直接引用)。
1.解析
在类加载的解析阶段,会将其符号引用转化为直接引用(入口地址),这类方法的调用称为“解析(Resolution)”。
在java中的解析调用:
1)静态方法
2)构造器方法
3)私有方法
4)final修饰的方法
2.静态分配调用(方法的重载)
1)方法调用选择静态类型的方式称为静态分派,看下面一个例子
package com.example.demo;public class Demo {/** * * 创建两个类 * */ static class Parent{} static class Son extends Parent{} /** * * 对show方法进行两种不同参数的重载 */ public static void show(Parent p) { System.out.println("这是Parent方法"); } public static void show(Son s) { System.out.println("这是Son方法"); } public static void main(String[] args) { //创建了一个Parent类,他的实例P的静态类型和实际类型都是Parent Parent p = new Parent(); //令p等于Son类型,此时P的静态类型为Parent,实际类型都是Son p = new Son(); //方法重载时,方法调用选择的是静态类型,所以此时调用的是Parent参数 show(p); //将P的静态类型强制转化为Son,此时P的静态类型为Son,实际类型都是Son //方法重载时,方法调用选择的是静态类型,所以此时调用的是Son参数 show((Son) p); } }
输出的结果是:
2)方法调用中,传递的实参的数据类型和方法的形参的数据类型类型不一定要一致,如果实参和形参的数据类型不同,则会优先选择与实参数据类型匹配度最高的形参数据类型方法进行调用,
需要注意的是:形参和实参的数据类型必须是能够相互转换的数据类型,看下面的一个例子
package com.example.demo;public class Demo { /** * * 对show方法进行多种不同参数的重载 */ public static void show(float a) { System.out.println("这是float方法"); }public static void show(Object b) { System.out.println("这是Object方法"); } public static void main(String[] args) { int a=0; String b="b"; show(a); show(b); } }
输出结果:
3.动态分派调用(方法的重写)
方法调用选择的实际类型的方式称为动态分派。
package com.example.demo;public class Demo {/** * * 创建一个父类和其show方法 * */ static class Parent{ public void show() { System.out.println("这是Parent方法"); } } /** * * 创建一个子类并重写父类的show方法 * */ static class Child extends Parent{ @Override public void show() { System.out.println("这是Child方法"); } } public static void main(String[] args) { //创建了一个parent实例,这个实例的静态类型为parent,实际类型为Child Parent parent=new Child(); //方法的重写选用的是实例的实际类型 parent.show(); } }
输出结果为:
参考文档: