简介
当字节码文件从磁盘加载到内存形成了java.lang.Class对象之后,就需要由虚拟机进行执行了。
对于所有的Java虚拟机的执行引擎在表面来看都是一致的:
运行时栈帧结构
栈帧,也叫过程活动记录,是用于支持虚拟机进行方法调用和方法执行的数据结构。
- 是java虚拟机内存中虚拟机栈的栈元素
- 包括了局部变量表、操作数栈、动态链接、方法返回地址等信息。
- 在编译过程中,局部变量表的大小已经确定,操作数栈深度也已经确定。因此栈帧在运行的过程中需要分配多大的内存是固定的。
在线程的方法调用中,可能调用链很长,很多方法都同时处于执行状态,但是对于执行引擎来说,活动线程中,只有栈顶的栈帧是最有效的,称为当前栈帧,对应的方法称之为当前方法。执行引擎所运行的字节码指令仅对当前栈帧进行操作。
栈帧概念结构:
局部变量表
局部变量表是用于存放方法参数和方法内部定义的局部变量的存储空间。
- 以变量槽(Variable Slot)为最小单位,每个Slot可以存放一个32位以内的数据类型
- 虚拟机规范只是说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据
- 对于64位的数据,使用连续两个Slot存储,第n和第n+1个,并且不允许采用任何方式单独访问其中的某一个。
- Slot允许重用。当字节码PC计数器超出了某个变量的作用域,即当某个变量再也不会被使用的时候,它对应的Slot就可以交给其他变量使用。
- 当Slot中的内容更新之后,需要有其他操作再访问一次局部变量表,才能让外界知道Slot中内容发生了更新。
操作数栈
操作数栈用于存放各种字节码指令。
- 后入先出栈
- 编译时确定最大深度
- 栈元素可以是任意的Java数据类型,包括long和double
- 32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2
当一个方法刚刚开始执行时,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。
虚拟机在操作数栈中存储数据的方式和在局部变量表中是一样的:如int、float、double、reference的存储。对于byte、short以及char类型的值在压入到操作数栈之前,会被转换为int
动态链接
每个栈帧都包含一个该栈帧所属方法的引用,此引用指向运行时常量池中的此栈帧对应的方法。持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
方法执行的指令后面就是跟的方法执行的符号引用作为参数
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
方法返回地址
一个方法开始执行后,只有两种方式可以退出这个方法:
- 执行引擎遇到方法返回的字节码指令,称之为正常完成出口
- 执行过程中已到了异常,且异常再方法体内没有得到处理,称之为异常完成出口
只有正常完成出口才有可能给它的上层调用者产生返回值。
在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
方法调用
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用
- 方法调用并不等同于方法的执行,方法调用阶段的唯一任务就是确定被调用方法的版本;
- 封装,继承,多态都会导致方法出现多个版本
- 静态方法不能被重写
- 静态成员(方法或属性)是类的成员存放在栈中,类可以直接调用(是属于类的静态成员,当然对象也可以调用,只是说可以使用而已);
- 实例成员是对象的成员,存放在堆中,只能被对象调用。
- 重写的目的在于根据创造对象的所属类型不同而表现出多态。因为静态方法无需创建对象即可使用。没有对象,重写所需要的“对象所属类型” 这一要素不存在,因此无法被重写
解析
方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。
解析主要包括两类方法:
- 静态方法,与类有关,不可能被继承或重写
- 私有方法,不可能被继承或重写
- 被final修饰的方法
分派
静态分派:主要针对方法的重载,编译器确定调用哪一个具体的方法
1 | Parent p1 = new Child1(); //child1是Parent的子类 |
这里的Parent就被称为是p1的静态类型,Child1是p1的实际类型
方法的调用是根据静态类型来选择的,就称为是静态分派
1 | p1 = new Child2(); |
动态分派:主要针对方法的重写。
- 找到操作数栈栈顶的第一个元素所指向的对象的实际类型,记为C
- 如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回非法访问异常
- 如果在类型C中没有找到,则按照继承关系从下到上依次对C的各个父类进行第2步的搜索和验证过程
- 如果始终没有找到合适的方法,则抛出抽象方法错误的异常 AbstractMethodError
从这个过程可以发现,在第一步的时候就在运行期确定接收对象(执行方法的所有者程称为接受者)的实际类型,所以当调用invokevirtual指令就会把运行时常量池中符号引用解析为不同的直接引用,这就是方法重写的本质。
参考
- 本文作者: xczll
- 本文链接: https://xczllgit.github.io/2020/03/30/jvm/2020-03-30-byteCodeExecuteEngine/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!