JVM (字节码执行引擎)

一,概述

在这里插入图片描述

二,运行时栈桢

1. 局部变量表

  • 作用

    局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

  • 存储

    局部变量表是以变量槽为最小单位存储(Slot),虚拟机规范中也没有指明一个Slot应该占用多大内存空间,
    对于32位的数据类型(byte、char、short、int、float、boolean、returnAddress),每个局部变量占用一个slot,而对于64位的数据类型(long、double)则需要占用两个slot,而reference类型可能是32位也可能是64位

    • 补充:
      在这里插入图片描述
  • 使用

    1. 虚拟机通过索引定位的方式使用局部变量表,索引值从0开始到局部变量表的最大slot数量,如果访问的是32位数据类型的变量,索引n就代表了使用第n个索引,如果访问的是64位的变量,那么会同时使用第n和第n+1两个slot,虚拟机不允许通过任何方式单独访问其中某一个slot,这种操作将在类加载的校验阶段抛出异常
    2. 在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那么局部变量表中的第0位索引的slot默认是用来传递该方法所属的对象实例的引用,在方法中可以通过 this 来访问这个隐含的参数,其他参数则按照参数列表的顺序排序,占用从1开始的局部变量slot,参数分配完成后,再根据方法体内部的变量顺序和作用域分配其余的slot
    3. 局部变量表中的slot可以复用

2.操作数栈

  • 简介

    操作数栈也成为操作栈,是一个后入先出的栈

  • 容量

    32位数据类型所占的栈容量是1
    64位数据类型所占的栈容量是2

  • 执行

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

  • 解释执行引擎

    java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中的栈就是操作数栈

3.动态连接

4.方法返回地址

  • 退出方法方式

    • 正常完成出口:第一种是执行引擎遇到任意一种方法返回的字节码指令,这种情况下可能会有方法返回值 传递给方法调用者

    • 异常完成出口:另外一种是执行过程中出现异常,但是方法体内又没有对异常进行处理,这种退出不会给调用者任何返回值

  • 返回地址

    • 正常退出 : 栈帧中会记录调用者的程序计数器值作为返回地址
    • 异常退出 : 返回地址需要通过异常处理器表来确定

三,方法调用

方法调用即指确认调用哪个方法的过程,并不是指执行方法的过程

1.解析

有几种方法的调用,在加载阶段就可以确认该方法的直接引用,前提是:方法在程序真正运行之前就有一个可确定的调用版本(调用哪一个方法),并且这个方法的调用版本在运行期是不可变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析。
有四种方法是进行的方法的解析:静态方法、私有方法、实例构造器、父类方法,这四类方法称为非虚方法,与之对应的就是续方法(final 方法除外),调用这四类方法的字节码指令是:invokestatic、invokespecial 指令,也就是说被 invokestatic、invokespecial 字节码调用的方法,在类加载的解析阶段就可以通过方法的符号引用确认方法的直接引用。
在 Java 字节码中,还有几种调用方法的字节码指令如下:

invokestatic:调用静态方法
invokespecial:调用实例构造器方法<init>、私有方法、父类方法
invokevirtual:调用所有的虚方法
invokeinterface:调用接口方法,会在运行时确认一个实现此接口的对象
invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。

被 final 关键字修饰的方法,在字节码中是被 invokevirtual 指令调用的,但是被 final 修饰的方法无法被重载或重写,所以只有一个方法,在加载阶段就可以确认调用哪个方法,所以也是一种虚方法,方法调用时走的也是解析流程。

2.分派

静态分派—(重载本质)

依据静态类型来定位方法执行版本的是静态分派

虚拟机(准确来说是编译器)在重载时会通过参数的静态类型而不是实际类型作为判断依据,并且静态类型是编译器可知的,所以在编译阶段,javac编译器会根据参数的静态类型决定使用哪个重载方法。

动态分派—(重写本质)

在运行时通过实际类型来决定方法执行版本的分派方式就称为动态分派

补充:
invokevirtual 的查找过程如下所示:

  • 1)找到操作数栈顶的引用所指的对象的实际类型,记做 C
  • 2)在类型 C 中查找与常量中的描述符和简单名称相同的方法,如果找到则进行访问权限的判断,如果通过则返回这个方法的直接引用,查找结束;如果权限不通过,则返回 java.lang.IllegalAccessError 的异常
  • 3)如果在 C 中没有找到描述符和简单名称都符合的方法,则按照继承关系从下往上依次在 C 的父类中进行查找和验证过程
  • 4)如果最终还是没有找到该方法,则抛出 java.lang.AbstractMethodError 的异常
单分派 多分派

分派根据基于多少种总量,可以分为单分派和多分派。总量是指:方法的接收者和方法的参数。根据分派时依据的宗量多少,可以分为单分派和多分派。
到目前为止,Java 语言还是一门 “静态多分派、动态单分派” 的语言,也就是说在执行静态分派时是根据多个宗量判断调用哪个方法的,因为在静态分派时要根据不同的静态类型和不同的方法描述符选择目标方法,在动态分派的时候,是根据单宗量选择目标方法的,因为在运行期,方法的描述符已经确定好,invokevirtual 字节码指令根据变量的实际类型选择目标方法。

四,基于栈的字节码解释执行引擎

解释执行: 代码由生成字节码指令之后,由解释器解释执行
编译执行: 通过即时编译器生成本地代码执行
在这里插入图片描述

基于栈的指令集和基于寄存器的指令集

基于栈的指令集中的指令是依赖于操作数栈运行的,基于寄存器的指令是依赖于寄存器进行工作的。

基于栈

  iconst_1 	
  iconst_1	//先将两个操作数进栈
  iadd		//遇到加指令后出栈相加后结果进栈
  istore_0

基于寄存器

  //mov 指令将寄存器 eax 中的值设置为 1,然后执行 add 指令将寄存器 eax 中的值加 1,结果就保存在 eax 寄存器中
  mov  eax,1
  add   eax, 1

区别:

  • 可移植:寄存器由硬件决定,限制较大,但是虚拟机可以在不同硬件条件的机器上执行
  • 代码相对更加紧凑:字节码中每个字节就对应一条指令,而多地址指令集中还需要存放参数
  • 编译器实现更加简单
  • 基于栈的指令缺点就是执行速度慢,因为虚拟机中操作数栈是在内存中实现的,频繁的栈访问也就意味着频繁的访问内存,内存的访问还是要比直接操作寄存器要慢的

猜你喜欢

转载自blog.csdn.net/weixin_41922289/article/details/89623857
今日推荐