JVM虚拟机栈& 本地方法栈

两种架构模型简介

  1. 栈的指令架构模型: 用于资源受限的系统, 操作只有入栈(压栈)和出栈, 使用0地址指令, 因此无需考虑地址分配
  • 优点: 移植性强适合跨平台设计
  • 缺点: 指令集小(每8位进行对齐), 所以每次操作需要更多的指令, 因此性能上不如寄存器架构模型
  1. 寄存器架构模型: 典型的应用是x86的二进制的指令集 如传统 PC, Android的 davlik虚拟机. 指令集完全依赖于硬件, 是 CPU直接执行的
  • 优点: 效率高
  • 缺点: 移植性差, 硬件耦合度高, 16位指令对齐

虚拟机栈概述

  • Hotspot虚拟机是基于栈的指令架构. JVM支持多个线程并行的执行, 每个线程都对应一份虚拟机栈, 也是线程私有的. 主要作用是主管程序的运行

特点

  • 操作方式只有入栈和出栈
  • 没有垃圾回收的问题

区别与堆

  • 栈主要负责程序的运行, 而堆的主要作用是存储
  • 栈存储方法调用时产生的局部变量(如 8种基本数据类型, 对象引用地址), 而堆存储对象的实例
  • 栈没有 GC, 而堆有 GC
  • 栈是不同线程间是隔离的, 而堆是共享的

生命周期

  • 虚拟机栈是随线程的开始和结束而创建和销毁

可能出的异常(错误)

栈大小可以是固定大小, 也可以动态扩展

  • 固定大小时, 一旦超出了栈的大小, 就会抛出 StackOverflowError(栈溢出异常)
  • 当可以动态扩展时, 一旦没有足够的内存用于扩展, 就会抛出 OutOfMemoryError(内存溢出异常)
    * 配置选项: -Xss1m设置线程栈深度
    * 更多调优相关参考: https://blog.csdn.net/qcl108/article/details/103476424

栈的存储单位

  • 栈中保存的一个个栈帧(Stack Frame), 每个栈帧对应一个方法, 栈帧是栈的存储单位, 栈帧的大小影响栈的深度

栈帧的内部结构

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack), 又称表达式栈(Expression Stack)
  • 动态链接(Dynamic Linking), 又称指向运行时常量池的方法引用
  • 方法返回地址(Return Address)
  • 附加信息
    在这里插入图片描述
  1. 局部变量表
  • 主要存储方法的形参和方法体内的局部变量
  • 局部变量表又称局部变量数组(一个数字数组), 基本存储单元是 Slot(变量槽), 32位以内的数据类型只占一个 Slot(如 byte, short, char, boolean, float, int和 returnAddress类型), 64位类型占用两个 Slot(如 long, double). 通过索引可以获取变量值, 注 获取64位类型变量值时, 只需使用起始索引即可
  • 线程私有, 所以本身是不存在数据安全问题的, 不过多个线程修改指定线程上的变量时, 如果不考虑同步问题, 也会导致线程不安全的, 还有通过形参或返回(作用域不只在方法内部)的 StringBuilder变量也会导致线程不安全
  • 局部变量表所需要的容量大小是在编译时已确定下来的(就是局部变量数字数组的长度), 并保存在方法的 code属性下的 maximum local variables数据项中. 在方法运行期间是不会再改变局部变量表的大小
  • 方法的嵌套调用次数由栈的大小来决定. 一般, 栈越大, 方法嵌套调用次数越多, 然后形参和局部变量大小决定栈帧的大小, 同时会影响方法嵌套调用次数
  • 局部变量表中的变量只在当前方法调用中有效. 当方法执行结束时, 随着方法栈帧的销毁, 局部变量表也会随之销毁
  1. 操作数栈
  • 开始执行了一个方法, 会随之创建一个新的栈帧, 然后根据字节码指令, 往栈中写入或提取数据, 即入栈/出栈, 在方法体内再调用了其它方法同时有返回值的话, 其返回值将会被压入当前栈帧的操作数栈中
  • 主要用于保存计算过程的中间结果, 作为计算过程中的变量临时存储空间
  • 操作数栈是数组结构, 不过不能通过索引访问, 而只能通过出入栈方式访问数据. 其栈大小(深度)是编译时已设定好的

演示代码


public class TestStack {
    public void testMethod() {
        byte a = 15;
        int b = 10;
        int c = a + b;
    }

    public int testSum() {
        int a = 10;
        int b = 1000;
        int c = a + b;
        return c;
    }

    public void getTestSum() {
        int d = testSum();
        int e = 10000;
    }
}

字节码执行过程图示

在这里插入图片描述
在这里插入图片描述

  1. 动态链接
  • Java源文件被编译到字节码文件时, 将所有的变量和方法引用都作为符号引用保存在字节码文件的常量池里. 动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用(为了指令便于识别, 又剩空间)
  • 程序运行后, 将字节码文件中的常量池(Constant pool)放到方法区的运行时常量池里
  • 每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用. 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
  1. 方法返回地址
  • 保存调用方法的 PC寄存器的值方法返回地址
  • 一个方法的退出, 有两种方式: 正常退出和非正常退出(异常退出)
  • 无论通过那种方式退出, 在方法退出后都返回到该方法被调用的位置. 正常退出时, 当前 PC计数器的值作为返回地址. 而异常退出时, 不会返回任何值
  1. 附加信息
  • 虚拟机规范中, 栈帧中还允许携带与 Java虚拟机实现相关的, 对程序调试提供支持的信息

本地方法栈概述(Native Method Stack)

并不是所有的 JVM都支持本地方法, 因为 Java虚拟机规范上, 并没有明确要求本地方法的使用语言和具体实现方法. Hotspot VM是本地方法栈和虚拟机栈合二为一的虚拟机

  • 本地方法栈是管理本地方法运行的, 本地方法是通过 C语言实现的, 在 Execution Engine执行时加载本地方法库(Native Method Library)
  • 与虚拟机站相同: 没有 GC, 不同线程间是隔离的(线程私有的)

本地方法

  • 当调用了一个本地方法时, 就会进入不再受虚拟机限制的环境, 级别与虚拟机一样, 所以可以访问任何虚拟机内部的运行时数据区
  • 可以直接使用本地处理器的寄存器
  • 可以直接使用本地内存

如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

猜你喜欢

转载自blog.csdn.net/qcl108/article/details/108563245