深入理解JVM—虚拟机字节码执行引擎

一、概述

虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。

二、运行时栈帧结构

Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。一个栈帧需要分配多少内存,并不会受到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式(编译时已决定,并写入到方法表的Code属性中)。

一个线程中的方法调用链可能会很长,以Java程序的角度来看,同一时刻、同一条线程里面,在调用堆栈的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”(Current Stack Frame),与这个栈帧所联的方法被称为“当前方法"(Current Method)。典型的栈帧结构如下。

 

1.局部变量表

局部变量表(Local Variables Table)是⼀组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最⼩单位。可存基本数据类型和引用类型。

局部变量不像前面介绍的类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值,不会产生歧义。但如果一个局部变量定义了但没有赋初始值,那它是完全不能使用的。

  1. 操作数栈

与局部变量表一样,均以字长为单位的数组。不过局部变量表用的是索引,操作数栈是弹栈/压栈来访问。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈。

java虚拟机栈是方法调用和执行的空间,每个方法会封装成一个栈帧压入占中。其中里面的操作数栈用于进行运算,当前线程只有当前执行的方法才会在操作数栈中调用指令(可见java虚拟机栈的指令主要取于操作数栈)

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

  1. 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。因为Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。

  1. 方法返回地址

当一个方法执行后,退出的方式有2种,一种是正常退出,一种是异常退出.无论哪种退出方式,都必须要返回到调用者位置,代码才会继续执行.方法正常退出时,主调方法的程序计数器值可以作为返回地址栈帧中很可能会保存这个值,当方法异常退出时,会通过异常处理器表来记录,栈帧中不会有记录

方法退出的过程,相当于栈帧的出栈,退出时的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入到调用者的操作数栈中,调整pc计数器的值指向方法调用指令后一条指令

  • 方法调用

方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(也就是之前说的直接引用)。

解析:

 (1)定义

所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用:在类加载的解析阶段,会将其中一部分符号引用转化为直接引用。

这种解析能成立的前提是:方法在程序真正运行前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好,编译器进行编译时就必须确定下来。这类方法的调用称为解析。

(2)编译期可知,运行期不可变

在java语言中符合“编译期可知,运行期不可变”这个要求的方法主要包括:

静态方法,与类直接相关

私有方法,在外部不可被访问

这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。

解析调⽤⼀定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及 的符号引⽤全部转变为明确的直接引⽤,不必延迟到运行期再去完成。而另⼀种主要的⽅

法调⽤形式: 分派(Dispatch)调⽤则要复杂许多,它可能是静态的也可能是动态的,按照

分派依据的宗量数可分为单分派和多分派

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

欣欣uvo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值