深入理解Java虚拟机之虚拟机栈

目录

出现背景

内存中的栈和堆

Java虚拟机栈是什么?

生命周期

栈运行原理

栈帧的内部结构

 

作用 

开发过程中,关于虚拟机栈可能的异常?

基于以上基础知识,尝试回答以下几个问题


出现背景

由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。

优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

内存中的栈和堆

栈是运行时的单位,堆是存储的单位。

即,栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放在哪里。

Java虚拟机栈是什么?

Java虚拟机栈,早期也叫Java栈,每个线程在创建是都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次Java方法调用,也就是说,栈中的数据都是以栈帧的格式存在的,在这个线程上正在执行的每个方法都各自对应一个栈帧。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

生命周期

虚拟机栈的生命周期同线程一致

栈运行原理

  1. 不同线程所包含的栈帧是私有的,不允许存在相互引用,即,不可能在一个栈帧之中引用另一个线程的栈帧。
  2. 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当期啊栈帧。
  3. Java方法有两种返回函数的方式 。一种是正常的函数返回,使用return指令,另一种是如果出现未经捕捉的异常,则已抛出异常的形式返回。不管使用哪种方式,都会导致栈帧被弹出。

栈帧的内部结构

1. 局部变量表

StartPC:变量的作用域起始字节码指令位置,Length:作用域长度;StartPC+Length=总字节码指令数
  • 局部变量表也被称之为局部变量数组或者本地变量表。
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,包括8种基本数据类型、对象引用以及returnAddress类型。
  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
  • 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对于一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求,进而函数调用就会占用更多的栈孔家,导致其嵌套调用次数就会减少。
  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,局部变量表随着方法栈帧的销毁而销毁。
  • 局部变量表最基本的存储单元是Slot(变量槽),其中,32位以内的类型只占一个slot(包括returnAddress类型),64位的类型(long和double占用两个slot);byte、short、char在存储前被转换为int,boolean也被转化为int,0表示false,非0表示true。JVM会为每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值,如果要访问一个64bit的局部变量值时,只需要使用两个slot中的第一个slot的索引即可。如果当前栈帧是由构造方法或者实例方法创建的,那么该对象引用this将会放在index=0的slot处,其余的参数按照参数表顺序继续排列,这也就解释了为什么静态方法中不可以引用this,因为this变量(当前对象的引用)不存在于静态方法的局部变量表中。此外,栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

  • 在栈帧中,与性能调优关系最为密切的部分就是局部变量 表。在方法执行时,虚拟机使用局部变量吧完成方法的传递。局部变量表中的变量也是重要的垃圾回收的根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

2. 操作数栈

每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出的操作数栈,在方法执行过程中,根据字节码指令,往操作数栈中写入数据或提取数据,即入栈/出栈。如果说Java虚拟机的解释引擎是基于栈的执行引擎,其中栈指的就是操作数栈。

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

  • 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值。
  • 栈中任何一个元素都是可以任意的Java数据类型,其中,32bit类型占用一个栈单位深度,64bit类型占用两个。
  • 操作数栈不同于局部变量表,并非采用访问索引的方式来进行数据访问,而是通过标准的入栈出栈操作来完成一次数据访问。
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

       讲到这里的话可能会有点懵,不是说局部变量表示储存方法参数和定义在方法体内的局部变量的吗,怎么又要从操作数栈里存取数据了?B站有一个逻辑清晰的代码追踪模拟可供参考:https://www.bilibili.com/video/BV1PJ411n7xZ?p=53。这里还涉及到Java字节码指令,所以这一方面首先要有基础。

3. 动态链接(或指向运行时常量池的方法引用)

每一个栈帧内部都包含一个指向运行时常量池(运行时常量池是在方法区里的)中该栈帧所属方法的引用。这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如:invokedynamic指令。

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如:描述一个方法调用了另外的其它方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

常量池的作用就是为了提供一些符号和常量以便于指令的识别,以节约内存。

4. 方法返回地址(或方法正常退出或者异常退出的定义)

存放调用该方法得到PC寄存器的值、

一个方法的结束,有正常执行完成和出现未处理的异常从而非正常退出两种方式,无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条地址。如果是异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分的信息。

本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

6. 一些附加信息

 

作用 

主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。

开发过程中,关于虚拟机栈可能的异常?

Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的

  • 如果采用固定大小的虚拟机栈,拿每一个线程的Java虚拟机栈容量可以再线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机会抛出一个StackOverflowError的异常。在开发过程中,不合理的递归方法就会导致这个问题。
  • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMerroyError异常。

设置栈内存的大小

我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

可以看到在默认情况下栈深度是在11417左右
可以看到在默认情况下栈深度是在11417左右

 

 在run==>Edit Configuration下可以进行设置。

基于以上基础知识,尝试回答以下几个问题

1. 举例栈溢出的情况?(StackOverflowError)  -Xss设置栈的大小:OOM

2. 调整栈大小,就能保证不出现溢出吗?   不能
3. 分配的栈内存越大越好吗?   不会,挤占其它区域的空间

4. 垃圾回收是否会涉及到虚拟机栈?   不会

5. 方法中定义的局部变量是否线程安全?具体问题具体分析,如果是线程内部产生内部消亡的,那一定是线程安全的,如果是外部传入或者是要返回到外部的局部变量,是线程不安全的,就是要注意局部变量的生命周期。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值