当前位置:   article > 正文

JVM运行时数据区_虚拟机栈是先进先出吗

虚拟机栈是先进先出吗

原文章:阿Q说代码 : 20张图助你了解JVM运行时数据区,你还觉得枯燥吗?

个人学习

面试题分析

(1)为什么要使用PC寄存器记录当前线程的执行地址呢?

JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,这时候就需要PC寄存器来记录某个线程的字节码执行位置,如果虚拟机是单线程也就没必要用程序计数器记录每个线程的位置了。

(2)PC寄存器为什么会被设定为线程私有呢?

由于jvm的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然就是为每一个线程都分配一个PC寄存器。这样各条线程之间计数器互不影响,独立存储。

虚拟机栈

1.栈的介绍

  • 它遵循“先进先出、后进后出”的原则
  • 优点就是跨平台、指令集小,编译器更容易实现。

堆和栈的区别

  • 栈:运行时的单位,它解决的是程序运行的问题,即程序如何执行,或者说是如何处理数据

  • 堆:存储的单位,它解决的是数据存储的问题,即数据怎么放、放在哪

    • 栗子:假如你正在修理汽车,我们可以把修车的操作步骤看做是栈操作,而把汽车的零件一个个放到汽车中就可以看做是堆存储

2.虚拟机栈介绍

早期叫Java栈,每个线程在创建时都会创建一个虚拟机栈,所以虚拟机栈是线程私有的,当线程结束时虚拟机栈也就结束了。

JVM对虚拟机栈的操作只有进栈出栈,所以它的访问速度仅次于程序计数器,也是一种快速有效的分配存储方式。

虚拟机栈来说它不存在垃圾回收问题,但是虚拟机栈的大小是动态的或者固定不变的,所以它会存在栈溢出或者内存溢出问题

  • 栈溢出:如果采用固定大小的虚拟机栈,那每一个线程的虚拟机栈容量可以在线程创建的时候独立选定
    • 如果线程请求分配的栈容量超过虚拟机栈允许的最大容量,虚拟机栈会抛出StackOverflowError异常
  • 内存溢出:如果虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程没有足够的内存去创建对应的虚拟机栈,那虚拟机将会抛出OutOfMemoryError异常。
    • 栈的大小直接决定了函数调用的最大可达深度,我们可以通过-Xss参数来配置栈内存,追加字母k或K表示KB,m或M表示MB,g或G表示GB,示例:-Xss1m

栈帧的运行原理

虚拟机栈主管Java程序的运行,保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回,那它内部到底是什么构造呢?虚拟机栈内部保存着一个一个的栈帧(Stack Frame),每个栈帧与该线程正在执行的每个方法都是一一对应的。

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

在一条活动线程中,一个时间点上,只会有一个活动的栈帧。

即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为**当前栈帧 **(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。

执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

执行过程

在这里插入图片描述

先进先出、后进后出

Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令(包含void返回类型);一种是抛出异常(指的是未处理的异常,如果是try...catch过了,算第一种)。不管使用哪种方式,都会导致栈帧出栈。不同线程中所包含的栈帧是不允许存在互相引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。

局部变量表 Local Variables

局部变量表也被称之为局部变量数组本地变量表,实际上是一个“数字”数组,主要用于存储方法的参数和定义在方法体内的局部变量(包括各类基本数据类型、对象引用、returnAddress类型),虚拟机使用局部变量表完成方法返回。

因为局部变量表是建立在线程的虚拟机栈上,是线程的私有数据,所以不会存在数据安全问题。

另外栈帧的大小主要受局部变量表的影响,而局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maxmum_local_variables数据项中,所以在方法运行期间是不会改变局部变量表的大小的,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

在虚拟机栈大小固定的前提下,它的局部变量表越大,它的栈帧就越大,那它的嵌套调用次数(方法调用数)也就越少,即栈的深度越浅

局部变量表中的数据只有在当前方法中有效。

在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

Slot

参数的存放总是在局部变量数组的索引0开始,到数组长度减1的索引结束,它最基本的存储单元就是Slot(变量槽)。

当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上。

JVM会为局部变量表中的每个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。其中32位以内的类型只占用一个slot(包含returnAddress类型,byteshortcharfloat都转化为int类型,而boolean类型是0为false,非0为true),64位的类型(longdouble)占用两个slot。如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。

在这里插入图片描述

如果当前帧是由构造方法或者实例方法创建的,那么该对象的引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列,而this变量不存在于静态方法的局部变量表中,所以上文中的main方法中不存在this变量。

在这里插入图片描述

另外Slot是可以复用的,如果一个局部变量过了其作用域,那么在其作用域之后声明的新的局部变量就很有可能会复用该局部变量的slot,从而达到节省资源的目的。

在这里插入图片描述

补充知识点:变量按照在类中的位置可以分为成员变量局部变量,其中成员变量又分为类变量和实例变量。

  1. 成员变量在使用前,都会默认初始化赋值,其中类变量是在类加载子系统的准备阶段进行默认赋值,在初始化阶段显示赋值;
  2. 实例变量会随着对象的创建,在堆空间中分配实例变量空间,并进行默认赋值;
  3. 局部变量是不会进行默认赋值的,所以在使用前必须进行显示赋值,否则编译不通过。

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或者间接引用的对象都不会被回收。

操作数栈 Operand Stack

操作数栈又称为表达式栈,在方法执行的过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈出栈

操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时存储空间

每一个操作数栈都会拥有一个明确的栈深度用于存储数据值,其所需要的最大深度在编译期间就定义好了,保存在方法的code属性中,为max_stack的值(与上边局部变量表类似)。

栈中的元素可以是任意的Java数据类型,其中32bit的用一个栈单位深度,64bit的用两个栈单位深度。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

我们所说的Java虚拟机的解释引擎是基于栈的执行引擎,其中的指的就是操作数栈。

阿Q特地制作了一张动态图来说明一下字节码指令执行时PC寄存器、局部变量表和操作数栈的运行过程:

在这里插入图片描述

在编译期间局部变量表和操作数栈的大小已经确定了:

  1. 首先将要执行的指令地址0存放到PC寄存器中,此时,局部变量表和操作数栈的数据为空;
  2. 当执行第一条指令bipush时,将操作数15放入操作数栈中,然后将PC寄存器的值置为下一条指令的执行地址,即2
  3. 当执行指令地址为2的操作指令时,将操作数栈中的数据取出来,存到局部变量表的1位置,因为该方法是实例方法,所以0位置存的是this的值,PC寄存器中的值变为3;
  4. 同步骤2和3将8先放入操作数栈,然后取出来存到局部变量表中,PC寄存器中的值也由3->5->6
  5. 当执行到地址指令为678时,将局部变量表中索引位置为12的数据重新加载到操作数栈中并进行iadd加操作,将得到的结果值存到操作数栈中,PC寄存器中的值也由6->7->8->9
  6. 执行操作指令istore_3,将操作数栈中的数据取出存到局部变量表中索引为3的位置,执行return指令,方法结束。

如果被调用的方法带有返回值,其返回值会被压入当前栈帧的操作数栈中,并更新pc寄存器中下一条需要执行的字节码指令。

栈顶缓存技术:将栈顶的元素全部缓存到物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

动态链接 Dynamic Linking

在Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。

在这里插入图片描述

当字节码文件被加载到虚拟机后,字节码文件中的一些数据,如类型信息、域信息、方法信息等,就会被放置到方法区中,而字节码文件中的常量池则会进入方法区中的运行时常量池。每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。

动态链接就是在“类加载”中“链接”的“解析阶段”将符号引用转化为直接引用的过程。

在这里插入图片描述

为什么字节码文件需要常量池?因为字节码文件需要数据支持,通常这种数据会很大,以至于不能直接存放到字节码中,换一种方式,可以将指向这些数据的符号引用存到字节码文件的常量池中,这样字节码只需使用常量池就可以在运行时通过动态链接找到相应的数据并使用。

方法返回地址 Return Address

方法返回地址是用来存放调用该方法的PC寄存器的值的。

方法结束两种方式:

  • 正常执行完成
  • 未处理的异常,非正常退出

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

方法正常退出时,当前线程的pc寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址。

而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息。

本质上,方法的退出就是当前栈帧出栈的过程

需要恢复上层方法的局部变量表、操作数栈,将返回值压入调用者栈帧的操作数栈,设置PC寄存器的值等,让调用者方法继续执行下去。

按照方法完成出口方式的不同又分为正常完成出口和异常完成出口:

  • 正常完成出口的字节码指令中的返回值类型为ireturnbooleanbytecharshortint)、lreturnlong)、freturnfloat)、dreturndouble)、areturn(引用类型)和return(void、实例初始化方法、类和接口的初始化方法)。
  • 在方法执行过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常处理表中没有搜索到匹配的异常处理器,就会导致方法的退出,简称异常完成出口。异常处理表是用来存储方法执行过程中抛出异常时的异常处理的,方便在发生异常的时候找到处理异常的代码。

两种方式的本质区别就是异常完成出口退出时不会给他的上层调用者产生任何的返回值。

提示:

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息。

本地方法 Native Method

本地方法是不在运行时数据区中的,如栗子:

在这里插入图片描述

本地方法其实就是java调用非java代码的接口,该接口由非java语言实现。本地接口的作用是融合不同的编程语言为java所用,它的初衷是融合C/C++程序。

在这里插入图片描述

native可以与所有其他的java标识符连用,但是abstract除外。

为什么要使用Native Method

  1. 与Java环境外交互:有时候java应用需要与java外边的环境进行交互;
  2. 操作系统进行交互:使用本地方法,我们可以用java实现jre与底层系统的交互;
  3. Sun’s Java:Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。

本地方法栈 Native Method Stack

本地方法栈是用来管理本地方法的调用的,也是线程私有的。他也允许被实现成固定或者可动态扩展的内存大小,在内存溢出方面与虚拟机栈类似。本地方法栈的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。

当某个线程调用本地方法时,他就进入了一个全新的并且不再受虚拟机限制的世界,他和虚拟机拥有同样的权限:

  • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区;
  • 可以直接使用本地处理器中的寄存区;
  • 直接从本地内存的堆中分配任意数量的内存。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/寸_铁/article/detail/975647
推荐阅读
相关标签
  

闽ICP备14008679号