JVM:虚拟机字节码执行引擎

《深入理解Java虚拟机》周志明第八章简要回顾

虚拟机字节码执行引擎

本章主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行

运行时栈帧结构

Java虚拟机以方法作为最基本的执行单元,“栈帧”则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,也是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行结束的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。

在编译Java源代码的时候,栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析计算出来,并且写入到方法表的Code属性之中。换言之,一个栈帧需要分配多少内存,并不会受到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式。

局部变量表

局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

局部变量表的容量是以变量槽为最小单位。一个变量槽可以存放一个32位以内的数据类型,Java中占用不超过32位存储空间的数据类型有boolean、byte、char、short、int、float、reference和returnAddress这8种类型。对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。Java语言中明确的64位的数据类型只有long和double两种。这里把long和double数据类型分割存储的做法与“long和double的非原子协定”中允许把一次long和double数据类型读写分割为两次32位读写的做法有些类似。

操作数栈

操作数栈也常被称为操作栈,是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占栈容量为2。Javac编译器的数据流分析工作保证了在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。

在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作与上面栈帧的部分局部变量表重叠在一起,这样不仅节约了一些空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无需进行额外的参数复制传递了。

Java虚拟机的解释执行引擎被称为“基于栈的执行引擎”,里面的“栈”就是操作数栈。

动态连接

每个栈帧都包含一个运行时常量池中该栈帧所属方法的引用,持有这个方法引用是为了支持方法调用过程中的动态连接。

方法返回地址

当一个方法开始执行之后,只有两种方法退出这个方法,第一种方式是执行引擎遇到任意一个返回的字节码指令,这时候可能会有返回值传递给上层方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回值来决定,这种退出方法的方式称为“正常调用完成”。

另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是Java虚拟机内部产生的异常,还是代码中使用athow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的。

无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。

方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。


方法调用

方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用了哪一个方法),暂时还未涉及方法内部的具体运行过程。

解析

所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。即:调用目标在程序代码写好、编译器进行编译的那一刻就应经确定了下来。这类方法的调用被称为解析。

Java语言中符合“运行期可知,运行期”不可变的这个要求的方法,主要有静态方法和私有私有方法量大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特性决定了它们都不可能通过继承或别的方式重写出其他版本,因此它们都适合在类加载阶段进行解析。

调用不同类型的方法,字节码指令集里设计了不同的指令。在Java虚拟机支持一下5条方法调用字节码指令:

  • invokestatic 调用静态方法
  • invokespecial 调用实例构造器()方法、私有方法和父类方法中的方法
  • invokevirtual 调用所有的虚方法
  • invokeinterface 调用接口方法,会在运行时再确定一个实现该接口的对象
  • invokedynamic

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual这个调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用,这些方法统称为“非虚方法”,与之相反,其他方法就被称为“虚方法”。

分派

解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成。而分派调用则要复杂的多,它可能是静态的,也可能是动态的。

1.静态分派

Human human= new Man();

上面代码中的Human称为变量的静态类型,或者叫外观类型,后面的Man则被称为变量的实际类型或者叫运行时类型。静态类型和实际类型在程序中都可能会发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译期在编译程序的时候并不知道一个对象的实际类型是什么。

虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据额。由于静态类型在编译期可知,所以在编译阶段,Javac编译器就可以根据参数的静态类型决定了会使用哪个重载版本。

所有依赖静态类型来决定方法执行的分派动作,都称为静态分派。静态分派最典型的应用表现在方法重载。静态分派发生在编译阶段,因此确定静态分派的动作不是由虚拟机来执行的。

2.动态分派

invokevirtual指令的运行时解析过程大致分为以下几步:

  1. 找到操作数在数栈顶的第一个元素所指向的对象的实际类型,记作C。
  2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
    正是因为invokevirtual指令执行的第一步就是运行期间确定接收者的实际类型,所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。我们把这种在运行期间根据实际类型确定方法执行版本的分配过程称为动态分派。

    多态性的根源在于虚方法调用指令invokevirtual的执行逻辑,所以只会对方法有效,对字段是无效的,因为字段不适用这条指令

3.单分派和多分派

方法的接收者和方法的参数统称为方法的宗量。单分派是根据一宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标进行选择。Java语言是一门静态多分派、动态单分派的语言。

4.虚拟机动态分派的实现
动态分派是执行非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在该接收者类型的方法元数据中搜索合适的目标方法,因此,Java虚拟机实现这种基于执行性能的考虑,真正运行时一般不会如此频繁地去反复搜索类型元数据。面对这种情况,一种基础而且常见的优化手段是为该类型在方法区中建立一个虚方法表,使用虚方法表来代替元数据查找以提高性能。


动态语言类型支持

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值