JVM学习整理——虚拟机栈详解

虚拟机栈概述

Java虚拟机栈(Java Virtual Machine Stack)和之前整理的PC寄存器一样,是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程的内存模型:每一个方法被执行的时候,Java虚拟机都会创建一个栈帧(Stack Frame),而每一个方法被调用直到执行完毕的过程,就对应着虚拟机栈从入栈到出栈的过程。
在这里插入图片描述

虚拟机栈存储结构以及运行原理

  1. 虚拟机栈是由一个一个的栈帧组成的,每次方法的执行都对应着栈帧入栈、出栈的操作。
  2. 栈帧出栈和入栈遵循“先进后出”/“后进先出”的原则。
  3. 在一条线程中,某一个时间点只会有一个正在活动的栈帧。即只有当前正在执行的方法的栈帧是有效的,这个栈帧叫做当前栈帧(Current Frame),与之对应的叫做当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。
  4. 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  5. 如果在当前方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈顶,成为新的当前栈。
  6. 当前线程请求的栈深度超过虚拟机栈最大深度的时候,会出现StackOverFlow异常,可以通过-Xss来设置栈空间的内存大小。
    在这里插入图片描述

栈帧中的组成部分

一个栈帧中主要有几个部分,分别是局部变量表(Local Variables),操作数栈(Operand Stack),动态链接(Dynamic Linking),方法返回地址(Return Address),一些附加信息,主要是前四个,下面也分别对四个部分进行解释。

局部变量表

  1. 局部变量表称为局部变量数组或者本地变量数组。
  2. 局部变量表存放了编译期可知的各种Java数据类型(boolean、byte、short、int、long、float、double),对象引用(reference类型,只是指向对象起始位置的一个引用指针)以及returnAddress类型(指向了一条字节码指令的地址)。
  3. 局部变量表是线程私有的数据,不存在数据安全问题。
  4. 局部变量表所需的容量大小是在编译期确定下来的,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定后的,在方法运行期间不会改变局部变量表的大小。这边具体就体现在方法Code属性的maximum local variables数据项中。
  5. 局部变量表最基本的存储单元是Slot(变量槽),变量槽也都是从索引为0开始计数。
  6. 局部变量表存储的数据类型中,64位长度的double,long类型会占据两个Slot,其他对象引用包括returnAddress都占据一个Slot。
    代码示例:
/**
 * 局部变量表测试
 * 
 * @author yuanzhihao
 * @since 2021/3/31
 */
public class StackTest1 {

    public static void main(String[] args) {
        // 除了64位长度的基本数据类型占用两个槽位 其他只占用一个槽位
        int i = 10;
        // long double 占两个槽位
        long j = 20; double k = 30.0;
        // 引用数据类型占一个槽位
        Demo demo = new Demo();
    }

    private void test1() {
        // 这边注意 非静态方法Slot的索引为0的位置是this 代表当前类对象
        int k = 20;
    }
    
    private static class Demo { }
}

使用javap -v StackTest1.class命令查看类文件信息,或者使用一个idea插件jclasslib也可以查看类文件信息。这边我是用的是javap命令和jclasslib插件,截取了部分截图。
在这里插入图片描述
对于非静态方法,Slot索引为0的位置存放的是this,代表当前对象。
在这里插入图片描述

操作数栈

  1. 操作数栈也被称为操作栈,它是一个后进先出(Last In First Out LIFO)栈。和局部变量表一样,操作数栈的最大深度也是编译的时候写入到Code属性的max_stacks数据项中。
  2. 操作数栈可以包含任意的Java数据类型,和局部变量表一样,long和double占两个栈容量,其它占一个栈容量。
  3. 当一个方法开始执行的时候,这个操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容。
  4. 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
  5. 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译期要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。
    下面用一段简单的代码和图示演示一下操作数栈的流程:
/**
 * 操作数栈测试
 *
 * @author yuanzhihao
 * @since 2021/3/31
 */
public class StackTest2 {

    public void testOperandStack() {
        int i = 10;
        int j = 5;
        int k = i + j;
    }
}

取出对应的字节码来详细的分析操作数栈的执行流程:在这里插入图片描述

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

  1. 每一个栈帧中都有一个指向运行时常量池中改栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。
  2. 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
    简单代码演示:
/**
 1. 动态链接测试
 2.  3. @author yuanzhihao
 4. @since 2021/4/1
 */
public class StackTest3 {

    public void testDynamicLinking() {
        String name = "haha";
    }
}

使用javap -v命令查看类信息,我这边截图简单说明一下:
在这里插入图片描述

方法返回地址

  1. 当一个方法开始执行的时候只有两种方式退出方法,一种是执行引擎遇到了任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,另一种就是方法执行过程中遇见了异常,并且异常没有被处理。
  2. 无论采用哪种退出方式,方法退出之后,都必须返回到最初方法被调用时的位置。一般来说,方法正常退出时,主调用方法的PC计数器的值就可以作为返回地址,即调用该方法的指令的下一条指定的地址。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧一般不保存这部分信息。
  3. 方法退出的过程就是出栈的过程,出栈的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈,调整PC寄存器的值指向方法调用指令的后面一条指令。

常见面试题

  1. 举例说明栈溢出的情况?
    当线程申请的栈的数量超过了虚拟机栈允许的最大深度,会出现StackOverFlowError栈溢出的异常。
  2. 调整栈的大小,就能保证不出现溢出吗?
    调整栈的大小并不能保证栈不溢出。只是可能会增大当前线程申请的栈的数量。
  3. 分配的栈内存越大越好吗?
    分配栈的内存越大的话,会挤压其他部分的内存大小,并不是分配的栈大小越大越好,需要按需调整。
  4. 垃圾回收是否会涉及到虚拟机栈?
    虚拟机栈只有出栈和入栈的操作,不存在垃圾回收的情况,栈中只会存在StackOverFlowError的异常,在我们常用的HotSpot虚拟机中,栈是不允许动态扩展的,所以也不涉及OutOfMemoryError。
  5. 方法中定义的局部变量是否线程安全?
    不一定,这边要看是否有多个线程操作局部变量,对应线程独占的变量是线程安全的,线程共享的变量就需要进行同步机制保证线程安全的问题。

结语以及参考链接

就是干 加油!
博客中代码地址:https://github.com/yzh19961031/blogDemo/tree/master/jvm
参考地址:
尚硅谷JVM全套教程(宋红康详解java虚拟机):https://www.bilibili.com/video/BV1PJ411n7xZ?p=35
深入理解Java虚拟机(第3版)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值