JVM-虚拟机栈和本地方法栈

虚拟机栈

Java 栈

Java 虚拟机栈:Java Virtual Machine Stacks,每个线程运行时所需要的内存

  • 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(一个方法一个栈帧

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

  • 虚拟机栈是每个线程私有的,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着:

    • 局部变量表:存储方法里的 Java 基本数据类型以及对象的引用
    • 动态链接:也叫指向运行时常量池的方法引用
    • 方法返回地址:方法正常退出或者异常退出的定义
    • 操作数栈或表达式栈和其他一些附加信息

设置栈内存大小:-Xss size -Xss 1024k

  • 在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M

虚拟机栈特点:

  • 栈内存不需要进行GC,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据

  • 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大)

  • 方法内的局部变量是否线程安全

    • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析)
    • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

异常:

  • 栈帧过多导致栈内存溢出 (超过了栈的容量),会抛出 OutOfMemoryError 异常
  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常

局部变量【locals】

局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量

  • 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题(但是逃逸情况下会不安全)
  • 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中
  • 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁
  • 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收

局部变量表最基本的存储单元是 slot(变量槽)

  • 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据
  • 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量
  • 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot
  • 局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的

操作数栈【stack】

栈:可以使用数组或者链表来实现

操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)

  • 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区

  • Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈

  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中

栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率

基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术

动态链接

动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是动态绑定

  • 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用

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

    常量池的作用:提供一些符号和常量,便于指令的识别

返回地址

Return Address:存放调用该方法的 PC 寄存器的值

方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置

  • 正常:调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址
  • 异常:返回地址是要通过异常表来确定

正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者

异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出

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

附加信息

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

本地方法栈

本地方法栈是为虚拟机执行本地方法时提供服务的

JNI:Java Native Interface,通过使用 Java 本地接口程序,可以确保代码在不同的平台上方便移植

  • 不需要进行 GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常

  • 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一

  • 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序

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

    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
    • 直接从本地内存的堆中分配任意数量的内存
    • 可以直接使用本地处理器中的寄存器

原理:将本地的 C 函数(如 foo)编译到一个共享库(foo.so)中,当正在运行的 Java 程序调用 foo 时,Java 解释器利用 dlopen 接口动态链接和加载 foo.so 后再调用该函数

  • dlopen 函数:Linux 系统加载和链接共享库
  • dlclose 函数:卸载共享库
文章作者: GeYu
文章链接: https://nuistgy.github.io/2023/06/05/JVM-虚拟机栈/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Yu's Blog