深入浅出 JVM 之运行时数据区-虚拟机栈

深入浅出 JVM 之运行时数据区-虚拟机栈

文章目录

  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。

系统环境:

  • JDK 1.8

参考地址:

深入浅出 JVM 系列文章

一、虚拟机栈概述

1.1 什么是虚拟机栈

Java 虚拟机中的 虚拟机栈 (Java Virtual Machine Stack) 是 Java 虚拟机运行时数据区中的一块线程私有的内存区域,逻辑上是一个 栈类型 结构的空间。

每当创建一个 线程 (Thread) 时,都会为其分配一个 虚拟机栈,用于存储线程的 栈帧 (Stack Frame)。每当线程调用一个方法时,就会创建一个新的栈帧并将其压入该线程的虚拟机栈中,用于存储方法执行过程中的局部变量和中间结果。并且当方法调用结束时,对应的栈帧会被弹出并销毁。

1.2 虚拟机栈的生命周期

虚拟机栈 的生命周期与 线程 的生命周期紧密相关。当创建一个线程时就会同时创建一个虚拟机栈,并且当线程销毁时,其对应的虚拟机栈也随之销毁。

1.3 虚拟机栈的作用

虚拟机栈 的主要作用是在 线程 执行方法时创建并管理 栈帧。也就是说,每当线程调用一个方法时,虚拟机栈会为该方法创建一个新的栈帧并将其压入栈顶。在这个栈帧中,会执行方法内的字节码指令,并存储方法的局部变量和其它相关信息。当方法执行完毕后,对应的栈帧会被弹出栈顶并销毁。

简而言之,虚拟机栈主要负责栈帧的 入栈出栈 操作,并且遵循 先进后出 / 后进先出 原则。

二、虚拟机栈的组成

2.1 栈帧 (Stack Frame)

■ 栈帧的组成

Java 虚拟机中的每个线程执行方法时,就会创建一个属于线程自身的虚拟机栈,而一个虚拟机栈主要由 栈帧 组成,栈帧是一块内存空间,主要由 局部变量表操作数栈动态链接返回地址 以及一些 附加信息 组成,通过这些组件实现了线程在执行方法时的一些计算操作和数据存储等功能。

栈帧组成示意图

注: 附加信息主要是用于记录实现 synchronized 的轻量级锁状态时,充当记录锁信息的 "锁记录空间" (displace record)。除此之外,还会记录一些对程序调试提供支持的信息。

■ 栈帧的生命周期

当一个 线程 调用一个 方法 时就会创建一个 栈帧,并将其压入该线程的虚拟机栈中。随后,该栈帧开始执行方法中的字节码指令。当方法 正常执行完成 或者 抛出异常 导致方法提前终止时,对应的栈帧会被弹出并销毁。

■ 栈中的当前栈帧

在线程执行过程中的任意一个时间点上,线程只会有一个活动的栈帧,即当前正在执行方法的 栈顶栈帧,这个栈帧被称为 当前栈帧 (Current Frame)。与当前栈帧对应的方法被称为 当前方法 (Current Method),而定义该方法的类则被称为 当前类 (Current Class)。

栈中的当前栈帧示意图

执行引擎只会运行 当前栈帧 中的字节码指令,如果在当前线程正在执行的方法中,存在需要调用其它方法的代码逻辑,那么当线程执行到该逻辑调用其它方法时,就会创建一个 新的栈帧,并将其压入虚拟机栈的顶,使之成为 新的当前栈帧

下面是一个简单的示例代码,展示方法调用时栈帧的变化过程:

 1public class StackFrameTest {
 2
 3    public static void main(String[] args) {
 4        m1(); // 调用方法1
 5    }
 6
 7    private static void m1() {
 8        System.out.println("方法-1 开始");
 9        System.out.println("-----------");
10        m2(); // 调用方法2
11        System.out.println("方法-1 结束");
12        System.out.println("-----------");
13    }
14
15    private static void m2() {
16        System.out.println("方法-2 开始");
17        System.out.println("-----------");
18        m3(); // 调用方法3
19        System.out.println("方法-2 结束");
20        System.out.println("-----------");
21    }
22
23    private static void m3() {
24        System.out.println("方法-3 开始");
25        System.out.println("-----------");
26        System.out.println("方法-3 结束");
27        System.out.println("-----------");
28    }
29
30}

当上述代码被执行时,输出的内容如下:

 1方法-1 开始
 2-----------
 3方法-2 开始
 4-----------
 5方法-3 开始
 6-----------
 7方法-3 结束
 8-----------
 9方法-2 结束
10-----------
11方法-1 结束
12-----------

从输出结果中可以看出,线程执行方法的顺序就是栈帧入栈和出栈的过程,遵循 先进后出 / 后进先出 的原则。

2.2 局部变量表 (Local Variable Table)

■ 局部变量表作用

局部变量表是一块数组类型的内存空间,主要用于存储 方法参数 以及在方法内部定义的 局部变量。这些变量通常是在方法执行期间使用的临时数据,它们的生命周期仅限于该方法的执行过程。

■ 局部变量表的特点

  • 局部变量表随线程调用方法而创建,随线程退出方法而销毁。
  • 每个线程拥有自己独立的局部变量表,因此不存在线程安全问题。
  • 局部变量表是栈帧中存储数据的主要区域,它的大小直接决定了栈帧的深度。
  • 局部变量表的数组容量在编译期间就已经确定,并且在方法执行期间不会动态改变其大小。

■ 局部变量表中的变量槽

局部变量表的容量的基本单元是 变量槽 (Slot),用于存储 方法参数局部变量 中的各种类型数据,比如:

  • 引用类型 reference (指向对象的引用);
  • returnAddress 类型 (表示返回地址的特殊类型);
  • 基本数据类型 boolean、byte、char、short、int、long、float、double;

并且不同类型的数据占用的槽数量也不相同,其中:

  • 32 位以内的类型只占用 1 个槽 (boolean、byte、char、short、int、long、float、double、reference、returnAddress 类型);
  • 64 位的类型占用 2 个槽 (long、double);

■ 变量槽存储的数据类型

不同类型数据的大小,以及占用槽的数量都是不相同的,具体汇总如下:

类型 长度 标志符 数据类型 描述
boolean 1位 Z 布尔类型 占1个槽位,存储基本类型的值
byte 8位 B 字节型 占1个槽位,存储基本类型的值
char 16位 C 字符型 占1个槽位,存储基本类型的值
short 16位 S 短整型 占1个槽位,存储基本类型的值
int 32位 I 整数型 占1个槽位,存储基本类型的值
long 64位 J 长整型 占2个槽位,存储基本类型的值
float 32位 F 单精度浮点型 占1个槽位,存储基本类型的值
double 64位 D 双精度浮点型 占2个槽位,存储基本类型的值
reference - L 引用类型 占1个槽位,存储引用的对象地址或句柄
returnAddress - - 引用类型 占1个槽位,存储 returnAddress 类型

上面的 reference 类型的变量,可以关联一个 句柄对象地址 等,以下是两种关联方式的示意图:

  • 栈中的变量与句柄关联

栈中的变量与句柄关联

  • 栈中的变量与对象地址关联

栈中的变量与对象地址关联

注1: 引用类型 reference 在虚拟机规范中没有明确说明它的长度,但通常情况下,虚拟机实现应当能够从这个引用中直接或间接地查找到对象在堆中的起始地址的索引和方法区中的对象类型数据。

注2: returnAddress 类型目前较为少见,主要为字节码指令 jsrjsr_wret 服务,用于指向一条字节码指令的地址。这些指令曾用于实现异常处理,但现在已经被异常表所替代。

■ 变量槽的特性

总的来说局部变量表中的变量槽有以下特性,归纳如下:

  • ① 32 位的数据类型仅占用 1 个槽,而 64 位的数据类型则占用 2 个连续的槽。
  • ② 在局部变量表中,方法参数的存放总是从局部变量数组索引 0 的位置开始。
  • ③ 当调用一个实例方法时,方法的参数和局部变量会按照它们在方法体中的声明顺序,被分配到局部变量表中的各个槽位上。
  • ④ Java 字节码中的局部变量,会通过局部变量表中的槽的访问索引 (Index) 进行关联,将基本类型的值或者引用类型的地址,赋到局部变量表中关联索引的变量。
  • ⑤ 变量槽中的槽位是可以重复使用的,当一个局部变量超出其作用域后,新声明的局部变量可以复用之前局部变量所占用的槽位,以此达到节省资源的目的。
  • ⑥ 在构造方法和实例方法中,局部变量表的 0 号槽位置用于存储 this 关键字,可以通过 this 访问实例对象的方法和属性。而在被 static 修饰的静态方法中,由于不存在 this 关键字,因此 0 号槽位置用于存储方法参数或局部变量。

接下来将分别对上述特性进行说明,以及画图描述,如下:

(1) 32 位的数据类型仅占用 1 个槽,而 64 位的数据类型则占用 2 个连续的槽。

 1package club.mydlq;
 2
 3public class Test {
 4
 5    void test() {
 6        // 占1个槽
 7        int a = 0;
 8        // 占2个槽
 9        long b = 0L;
10        // 占1个槽
11        int c = 0;
12    }
13
14}

(2) 在局部变量表中,方法参数的存放总是从局部变量数组索引 0 的位置开始。

 1package club.mydlq;
 2
 3public class Test {
 4
 5    void test() {
 6        int a = 0;
 7        long b = 0L;
 8    }
 9
10}

(3) 当调用一个实例方法时,方法的参数和局部变量会按照它们在方法体中的声明顺序,被分配到局部变量表中的各个槽位上。

 1package club.mydlq;
 2
 3public class Test {
 4
 5    void test() {
 6        // 实例方法在局部变量表中 Index 为 1
 7        int a = 1;
 8        // 实例方法在局部变量表中 Index 为 2
 9        int b = 2;
10        // 实例方法在局部变量表中 Index 为 3
11        int c = 3;
12    }
13
14}

注: 实例方法中 Index 为 0 的位置为 this,而静态方法中 Index 为 0 的位置为方法参数或者局部变量。

(4) Java 字节码中的局部变量,会通过局部变量表中的槽的访问索引 (Index) 进行关联,将基本类型的值或者引用类型的地址,赋到局部变量表中关联的索引的变量。

 1package club.mydlq;
 2
 3public class Test {
 4
 5    void test() {
 6        int a = 8;
 7        int b = 16;
 8        int c = a + b;
 9        String d = "test string";
10        Object e = new Object();
11    }
12
13}

通过 javap -c Test 命令查看字节码指令,可以看到:

 1public class club.mydlq.Test {
 2  public mydlq.club.Test();
 3    Code:
 4       0: aload_0
 5       1: invokespecial #1          // Method java/lang/Object."<init>":()V
 6       4: return
 7
 8  void test();
 9    Code:
10       0: bipush               8
11       2: istore_1
12       3: bipush               16
13       5: istore_2
14       6: iload_1
15       7: iload_2
16       8: iadd
17       9: istore_3
18      10: ldc                  #2   // String test string
19      12: astore               4
20      14: new                  #3   // class java/lang/Object
21      17: dup
22      18: invokespecial        #1   // Method java/lang/Object."<init>":()V
23      21: astore               5
24      23: return
25}

(5) 变量槽中的槽位是可以重复使用的。当一个局部变量超出其作用域后,新声明的局部变量可以复用之前局部变量所占用的槽位,以此达到节省资源的目的。

 1package club.mydlq;
 2
 3public class Test {
 4
 5    void test() {
 6        int a = 1;
 7        // 执行代码块中的代码
 8        {
 9            int b = a + 1;
10            int c = b + 1;
11            System.out.println(c);
12        }
13        // 执行代码块外的代码
14        int d = a + 1;
15    }
16
17}

(6) 在构造方法和实例方法中,局部变量表的 0 号槽位置用于存储 this 关键字,可以通过 this 访问实例对象的方法和属性。而在被 static 修饰的静态方法中,由于不存在 this 关键字,因此 0 号槽位置用于存储方法参数或局部变量。

 1package club.mydlq;
 2
 3public class Test {
 4
 5    void test1() {
 6        int a = 1;
 7    }
 8
 9    static void test2() {
10        int a = 2;
11        int b = 3;
12    }
13
14}

实例方法如下图所示:

静态方法如下图所示:

2.3 操作数栈 (Operand Stack)

■ 操作数栈是什么

操作数栈 是一个 LIFO (后进先出) 的栈空间,线程在执行方法过程中会根据字节码指令,往操作数栈中写入或获取数据,即入栈 push 和出栈 pop,所以很多时候操作数栈也被称之为表达式栈 (Expression Stack)。

线程执行某些字节码指令时,会将值压入操作数栈进行运算,比如执行复制、交换、求和等操作。还有些字节码指令执行时,会使数据从操作数栈中出栈,从而得到计算结果等。

■ 操作数栈作用

操作数栈的主要作用是在方法执行期间存储 中间计算结果,同时也用来保存过程中的 部分结果,并参与计算。

例如,在执行加法操作时,两个操作数会被推入栈中,然后执行相应的字节码指令,如 iaddladd,这些指令会从栈中弹出这两个操作数,执行加法运算,并将结果再次压入栈中。

■ 操作数栈特性

  • Java 虚拟机的解释引擎是一种基于栈的执行引擎,其中的核心数据结构之一就是操作数栈。
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并且还会更新程序寄存器中下一条需要执行的字节码指令;
  • 操作数栈中元素的数据类型,必须与字节码指令的序列严格匹配,这个验证过程会在编译器编译期间进行,并且在类加载过程中的 "类检验阶段" 的 "数据流分析阶段",还要进行二次验证。
  • 每一个操作数栈都会拥有一个明确的 栈深度 (也称为栈的高度),来表示当前栈中数据的大小,并且栈也有最大深度,栈最大深度在编译器编辑期间就定义好了,保存在字节码方法的 Code 属性中,为 max_stack 的值;
  • 栈中的任何一个元素都是可以任意的 Java 数据类型,32bit 的类型占用一个栈深度,64bit 的类型占用两个栈深度;

■ 操作数栈操作过程示例

为了方便理解操作数栈执行过程,这里给出一个示例代码,然后结合图文进行介绍,如下:

1public class Test {
2
3    public void a() {
4        byte i = 15;
5        int j = 8;
6        int k = i + j;
7    }
8
9}

编译成字节码后,显示如下:

 1public class mydlq.swagger.example.Test {
 2  public mydlq.swagger.example.Test();
 3    Code:
 4       0: aload_0
 5       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
 6       4: return
 7
 8  public void a();
 9    Code:
10       0: bipush        15
11       2: istore_1
12       3: bipush        8
13       5: istore_2
14       6: iload_1
15       7: iload_2
16       8: iadd
17       9: istore_3
18      10: return

那么线程在执行这些执行时,会将数据加入到操作数栈中进行逻辑计算,过程如下:

10: bipush        15       // 将常量 15 压入操作数栈中
22: istore_1               // 将常量 15 保存至变量 i 中
33: bipush        8        // 将常量 8 压入操作数栈中
45: istore_2               // 将常量 8 保存至变量 j 中
56: iload_1                // 加载变量 i, 并将 i 变量的值 15 放到操作数栈中
67: iload_2                // 加载变量 j, 并将 j 变量的值 8 放到操作数栈中
78: iadd                   // 将操作数栈的数据进行累加操作,即 8 + 15
89: istore_3               // 加法结果保存在变量 k 中
910: return                // 返回到执行方法前的字节码指令位置

执行流程如下:

(1)、首先执行第一条语句,程序寄存器指向的是 0,也就是指令地址为 0,然后执行 bipush 指令,将常量 15 推入操作数栈。

(2)、执行完后,使程序计数器中的值 +1,指向下一行代码,执行 istore_1 指令,将操作数栈中的元素存储到局部变量表 1 的位置,即赋给变量 i,这时我们可以看到局部变量表中已经增加了一个元素。

注: 局部变量表索引之所以从 1 开始,这是因为该方法为实例方法,上面介绍局部变量表时介绍过,在实例方法中,局部变量表索引 0 的位置存放的是 this

(3)、继续使程序计数器中的值 +1,指向下一行,执行 bipush 指令,将常量 8 推入操作数栈。

(4)、执行完后,使程序计数器中的值 +1,指向下一行代码,执行 istore_2 指令,将操作数栈中的元素存储到局部变量表 2 的位置,即赋给变量 j,这时我们可以看到局部变量表又增加了一个元素。

(5)、继续使程序计数器中的值 +1,指向下一行,执行 iload_1 指令,将变量 i 的值 15 压入操作数栈。

(6)、继续使程序计数器中的值 +1,指向下一行,执行 iload_2 指令,将变量 j 的值 8 压入操作数栈。

(7)、继续使程序计数器中的值 +1,指向下一行,执行 iadd 指令,将操作数栈中的两个元素执行相加操作,即 15+8 得到结果值 23

(8)、继续使程序计数器中的值 +1,指向下一行,执行 istore_3 指令,将操作数栈中记录的相加结果 23 存储在局部变量表 3 的位置。

最后继续使程序计数器中的值 +1,执行 return 指令返回结果,到此操作数栈执行计算过程就结束了。

■ 栈顶缓存技术

基于栈式架构的虚拟机所使用的 零地址指令 更加紧凑,完成一项操作时需要使用更多的 入栈出栈 指令,这也就意味着将需要更多的 指令分派次数内存读/写次数

由于 操作数 是存储在内存中的,因此频繁地执行 内存读/写 操作,必然会影响执行速度。为了解决这个问题,HotSpot JVM 的设计者引入了 栈顶缓存 (Top Of Stack Cashing) 技术,将栈顶元素全部缓存在物理 CPU 的寄存器中,这样可以减少所需的指令数量,并加快执行速度,以此来降低 内存读/写 次数,提升执行引擎的执行效率。

2.4 动态链接 (Dynamic Linking)

■ 符号引用、运行时常量池和直接引用

在探讨动态链接之前,我们先了解以下几个概念:

概念 描述
符号引用 在 Java 开发过程中,编写的代码都会编译为以 .class 为后缀的字节码文件,在字节码文件中的变量和方法引用,都是通过 "符号" 的形式来描述引用的地址 (本质上就是一段描述某个地址的字符串)。比如,通过符号的形式描述一个方法所调用的其它方法,这种通过符号来描述引用地址的方式我们统称为 "符号引用"。
运行时常量池 其实在字节码文件中,符号引用所关联的数据是存储在字节码的 "常量池" 内的。常量池存储的是一些静态信息,这些信息只有在被类加载器加载到 Java 虚拟机后,字节码文件中的符号引用才能生产对应的内存地址信息,这些常量池一旦被装入内存就变成 "运行时常量池"。

在运行时常量池中,主要存储的是类被加载后解析的字面量与符号引用,以及符号引用关联的直接引用地址。但不止这些,因为运行时常量池具备动态性,可以动态添加数据。
直接引用 直接引用是指在内存中指向目标对象的确切地址。例如,类中方法的实际内存地址即为直接引用。它是符号引用在运行时的具体实现形式。

■ 动态链接是什么

动态链接是指在程序运行期间将方法调用的符号引用转换为直接引用的过程。如果被调用的方法在编译期无法确定下来,而只能在程序运行期间根据具体情况确定,则这种转换过程就称为 "动态链接"。这种链接方式使得程序可以在运行时根据实际需要动态地查找并绑定方法,增强了程序的灵活性和可扩展性。


个人理解:

当我们编写 Java 代码并将其编译为字节码文件时,代码中定义的变量和方法引用是以 "符号" 的形式来描述的,并且这些符号被存储在字节码文件的 常量池 中。这里的 "符号" 实际上是一种描述引用位置的信息,而不是直接的内存地址。

当 Java 虚拟机加载字节码文件时,它会经历一个称为 链接 的过程,其中包括 解析阶段。在这个阶段,Java 虚拟机会将字节码文件中的符号引用转换为指向 运行时常量池 中的直接引用。这些直接引用指向实际的对象或方法。

例如,如果我们有一个类 A 中的方法调用了另一个类 B 中的方法,那么在编译后的字节码文件中,这种调用关系将通过符号来表示。当 Java 虚拟机加载这两个类时,它们会被实例化为对象,如 Method 对象,这些对象存储在运行时常量池中,代表已加载的类及其方法。这样,类和方法就有了具体的内存地址。

动态链接是一种机制,它允许在运行时将静态的 "符号引用" 转换成真实的 "内存地址"。这意味着当一个方法调用另一个方法时,最初使用的是静态的符号引用,而在动态链接过程中,这些符号引用会被转换为指向实际内存位置的直接引用。这种方法不仅增强了程序的灵活性,还允许在运行时确定方法的真正调用目标。


在每一个栈帧内部,都包含一个指向运行时常量池中属于该栈帧所属方法的引用。

■ 动态链接的作用

动态链接的主要作用是将方法的 符号引用 替换为运行时常量池中的方法的 直接引用。这样一来,栈帧 中的操作数栈就可以指向运行时常量池中的具体 方法引用地址,从而实现方法的实际调用。

2.5 返回地址 (Return Address)

■ 方法返回地址的作用

在 Java 中,正在执行方法的线程如果想结束当前方法,有两种结束方式,分别为 正常执行方法结束执行过程中出现异常结束 两种。

无论哪种方式,线程执行方法结束后,都应该回到调用该方法前的位置,所以 方法返回地址 主要是用于存储 当前线程程序计数器 中记录的 字节码指令位置,方便线程执行方法结束后,能够正确的回到调用方法前的字节码指令位置。

而之所以需要存储方法返回地址,这主要是因为程序计数器在执行过程中记录的指令位置一直都在改变,所以无法依靠程序计数器记录方法执行前的字节码指令位置。

■ 方法正常退出

线程调用方法完成正常退出后,这时线程会将记录在 栈帧 中的 方法返回地址 作为 程序计数器的值 ,然后将 程序计数器的值 作为 返回地址,线程只要执行下一条指令 (return相关指令),就可以返回到调用该方法前的指令位置。

在 Java 字节码指令中存在多种返回指令,而且不同的返回类型使用不同的返回指令,归总如下:

指令 对应的返回类型
ireturn boolean、byte、char、short、int
lreturn long
freturn float
dreturn double
areturn reference 引用类型
return 返回值类型为 void 的方法

这里给出一段方法正常退出的示例代码,如下:

1public class Test {
2
3    public int a(){
4        int i = 1;
5        int j = 2;
6        return i+j;
7    }
8
9}

编译成字节码后显示如下:

 1public class mydlq.swagger.example.Test {
 2  public mydlq.swagger.example.Test();
 3    Code:
 4       0: aload_0
 5       1: invokespecial #1  // Method java/lang/Object."<init>":()V
 6       4: return
 7
 8  public int a();
 9    Code:
10       0: iconst_1
11       1: istore_1
12       2: iconst_2
13       3: istore_2
14       4: iload_1
15       5: iload_2
16       6: iadd
17       7: ireturn  // 执行到该字节码指令后,就会返回到栈帧中记录的方法返回地址
18}

■ 方法异常退出

线程调用方法出现异常退出时,线程需要通过 异常表 中记录的数据来确定 方法的返回地址

这里给出一段方法异常退出的示例代码,如下:

 1import java.io.FileInputStream;
 2import java.io.IOException;
 3
 4public class Test {
 5
 6    public int a() {
 7        try {
 8            FileInputStream fis = new FileInputStream("d://test.txt");
 9        } catch (IOException e) {
10            e.printStackTrace();
11        }
12        return 0;
13    }
14
15}

编译成字节码后显示如下:

 1public class mydlq.swagger.example.Test {
 2  public mydlq.swagger.example.Test();
 3    Code:
 4       0: aload_0
 5       1: invokespecial #1   // Method java/lang/Object."<init>":()V
 6       4: return
 7
 8  public int a();
 9    Code:
10       0: new           #2   // class java/io/FileInputStream
11       3: dup
12       4: ldc           #3   // String d://test.txt
13       6: invokespecial #4   // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
14       9: astore_1
15      10: goto          18
16      13: astore_1
17      14: aload_1
18      15: invokevirtual #6   // Method java/io/IOException.printStackTrace:()V
19      18: iconst_0
20      19: ireturn
21
22    Exception table:         // 异常表
23       from    to   target   type
24        0      10     13     Class java/io/IOException
25}

三、虚拟机栈相关问题

3.1 虚拟机栈在什么情况下会发生栈内存溢出?

当一个线程请求的栈深度超过虚拟机允许的最大深度时,则会抛出 StackOverflowError。这种情况常发生在递归调用中,比如在递归调用中,递归的层数超过了虚拟机所能支持的最大值,则会导致此错误。

3.2 虚拟机栈什么情况下出现 OOM 错误吗?

当虚拟机无法为线程分配新的栈帧时,即当线程栈空间耗尽而无法扩展时,则会抛出 OutOfMemoryError。这通常是因为设置的虚拟机栈空间过小,或者是创建很多较大的线程栈导致的这个问题。

3.3 垃圾回收是否涉及到虚拟机栈?

垃圾回收器 (GC) 不直接处理虚拟机栈中的数据。GC 主要负责管理堆内存中的对象,而虚拟机栈中的数据结构 (如局部变量表) 是由线程私有且自动管理的,不需要 GC 干预。

3.4 方法中定义的局部变量是否线程安全?

具体问题具体分析。如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则可能是线程不安全的。

3.5 虚拟机栈占用的内存是堆内存还是堆外内存?

JVM 虚拟机栈通常是分配在堆外内存中。

因为 JVM 虚拟机栈是线程私有的,所以通常情况下它的内存分配和回收是由操作系统来完成的,而不是由 JVM 内部的垃圾回收机制来管理,这也是 JVM 虚拟机栈通常被分配在堆外内存中的原因之一,以便更高效地管理和使用内存。

---END---


  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。