虚拟机栈

一、简介

Java 虚拟机栈(Java Virtual Machine Stack) 是线程私有的,它的生命周期与线程相同,虚拟机栈描述的是 Java 方法执行的线程内存模型,每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到处栈的过程.

 

二、虚拟机栈内存模型

每个线程都会有对应的一个 Java 虚拟机栈,每一个方法对应的便是一个栈帧,在虚拟机栈顶的栈帧称为活动栈帧,对应的是当前正在执行的方法

一段示例代码

@Slf4j
public class Client {
    public static void main(String[] args) {
        int i = 10;
        int j = 100;
        double sum = method01(i, j);
        log.info("sum: {}", sum);
    }
    public static double method01(int i, int j) {
        int k = i + j;
        double d = 100;
        double sum = method02(k, d);
        return sum;
    }
    public static double method02(int j, double d) {
        double sum = (double) j + d;
        return sum;
    }
}

上述代码对应的 Java 虚拟机内存模型如下

1、开始执行 main 方法,main 方法对应的栈帧被压入 Java 虚拟机栈的栈底位置,此时正在执行 main 方法,虚拟机栈中只有 main 栈帧,这个时候 main 栈帧就是当前活动栈帧

2、接着调用 method01 方法,method01 栈帧也被压入 Java 虚拟机栈中,此时正在执行 method01 方法,虚拟机栈中有 main 栈帧、method01 栈帧,这个时候 method01 栈帧就是当前活动栈帧

3、最后调用 method02 方法,method02 栈帧也被压入 Java 虚拟机栈中,此时正在执行 method02 方法,虚拟机栈中有 main 栈帧、method01 栈帧、method02 栈帧,这个时候 method02 栈帧就是当前活动栈帧

4、method02 方法执行完毕,对应的 method02 栈帧从 Java 虚拟机栈中执行出栈操作,此时虚拟栈中有 method01 栈帧和 main 栈帧,这个时候 method01 栈帧就是当前活动栈帧

5、method01 方法执行完毕,对应的 methodd01 栈帧从 Java 虚拟机栈中执行出栈操作,此时虚拟机栈中只有 main 栈帧,这个时候 main 栈帧就是当前活动栈帧

6、main 方法执行完毕,对应的 main 栈帧从 Java 虚拟机栈中执行出栈操作,此时虚拟机栈中不存在任何栈帧,所有方法执行完毕,对应的 main 线程结束,Java 虚拟机栈也等待着被内存回收

 

三、栈帧

虚拟机栈描述的是 Java 方法执行的线程内存模型,整个线程在运行的过程中执行的方法便对应为一个个栈帧,栈帧主要存储 局部变量表、操作数栈、动态链接、方法出口等信息,其中局部变量表和操作数栈是比较重要的两个结构

1、局部变量表

局部变量表存放了编译期可知的各种 Java 虚拟机基本数据类型(boolean、byte、short、char、int、float、double、long)、引用数据类型,这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中 64 位的 long 和 double 类型的数据会占用两个变量槽,其余数据类型只占用一个槽位.局部变量表所需的内存空间在编译期间已经确定下来,在方法运行期间不会改变局部变量表的大小(这里所说的大小是指局部变量表的槽位数)

先看一段代码

public class Client {
    public double method01(int i, double j) {
        double k = i + j;
        return k;
    }
}

局部变量槽的大小在编译期就已经确定下来了

上图是通过 Jclasslib 反解析得出的,局部变量表的最大槽位数是 6

通过查看局部变量表的详细信息可以得知,对于非静态方法,局部变量表 slot0 位置存放的是 this 引用(当前类对象的引用指针),slot 1 位置存放的是 int 类型的变量 i,slot2 和 slot3 这两个槽位存放的是 double 类型的变量 j,slot4 和 slot5 这两个槽位存放的是 double 类型的变量 k,整个局部变量槽从 slot0~slot5,总共占据 6 个槽位,局部变量表分布如下

Jvm 会为局部变量表中的每一个 slot 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值

2、操作数栈

每个独立的栈帧中除了包含局部变量表以外,还包含了一个先进后出的操作数栈,也可以称为表达式栈,操作数栈在方法执行的过程中,根据字节码指令,往栈中写入数据或者提取数据

method01 对应的字节码指令如下

上述字节码指令的 iload_1 就是将局部变量表 slot1 位置的变量压入操作数栈中,dstore 4 就是将相加的结果从操作数栈中出栈,然后存入局部变量表 slot4 处

3、动态链接

动态链接的作用就是为了将符号引用转换为指向内存的直接引用

4、方法出口

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

5、案例

通过一段代码,来体会一下局部变量表和操作数栈之间的相互联动

public class Client {
    public static void main(String[] args) {
        int i = 10;
        i = ++i + i++;
        System.out.println(i);
    }
}

javap -verbose -p Client.class 反解析得到的字节码指令

 0 bipush 10
 2 istore_1
 3 iinc 1 by 1
 6 iload_1
 7 iload_1
 8 iinc 1 by 1
11 iadd
12 istore_1
13 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
16 iload_1
17 invokevirtual #3 <java/io/PrintStream.println : (I)V>
20 return

指令 1、bipush 10: 将常数 10 压入操作数栈栈顶位置

指令 2、istore_1: 将操作数栈栈顶的操作数(10)弹出操作数栈,存储在局部变量表 slot1 位置

指令 3、iinc 1 by 1: 将局部变量表 slot1 位置的变量自增 1(前面的 1 对应的是局部变量表的槽位,后面的 1 对应的是每次自增数的大小, 例如: iinc 2 by 1 就是将局部变量表 slot2 处的变量自增 1)

指令 4、iload_1: 将局部变量槽 slot1 位置变量压入操作数栈栈顶位置

指令 5、iload_1: 将局部变量槽 slot1 位置变量压入操作数栈栈顶位置

指令 6、iinc 1 by 1: 将局部变量表 slot1 位置的变量自增 1

指令 7、iadd: 将操作数栈栈顶的操作数和次栈顶的操作数弹出操作数栈,执行加法操作,并把相加后的结果重新压入操作数栈栈顶位置

指令 8、istore_1: 将操作数栈栈顶的操作数(22)弹出操作数栈,存入局部变量表 slot1 位置处

指令 9、 getstatic #2: 通过符号引用 #2 去运行时常量池中获取对应的直接引用(引用类型 java/io/PrintStream 的字段 java/lang/System.out),并把该引用压入操作数栈栈顶位置

指令 10、将局部变量表 slot1 位置变量压入操作数栈栈顶位置

指令 11、invokevirtual #3: 将 System.out 引用地址、操作 22 弹出操作数栈,利用 System.out 引用地址调用其返回值类型为 void 的方法 println(),并且将结果输出

指令 12、return: 返回值为 void 类型对应的返回指令

通过分析字节码指令,可以知道上述代码输出的最终结果是 22

 

四、Java 虚拟机栈的垃圾回收和内存溢出

Java 虚拟机栈是不存在垃圾回收的,因为随着方法的执行结束,对应的栈帧就会从 Java 虚拟机栈中弹出,不需要等垃圾回收器来进行垃圾收集

在 <<Java 虚拟机规范>> 中,对于Java 虚拟机栈规定了两类异常情况

1、如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverFlowError 异常

2、如果 Java 虚拟机栈容量可以动态扩展(即存在最小虚拟机栈空间和最大虚拟机占空间),当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常

注意: Java 虚拟机栈支持不支持动态扩展,这个是要看具体的虚拟机实现的,我们常用的 Hotspot 虚拟机是不支持 Java 虚拟机栈动态扩展的,也就是说 Hotspot 虚拟机的虚拟机栈只会有 StackOverFlowError,而不会有 OutOfMermoryError 出现

StackOverflowError 案例演示

通过上面的介绍,可以得知,如果想要使 Java 虚拟机栈出现 StackOverFlowError,那么有两种方式

方式一、栈帧的数量大于 Java 虚拟机栈的最大深度

方式二、局部变量表的变量过多,导致单个栈帧占用内存过大

案例一、模拟栈帧数量过多导致的 StackOverflowError

在执行代码之前需要设置一下 Java 虚拟机栈的最大内存,对应的虚拟机参数如下

// 设置虚拟机栈最大内存空间为 128k
-Xss128k

需要注意的是 Xss 的大小是有限制的,我这里的环境是 JDK8 hotspot 虚拟机,提示分配的 Java 虚拟机栈最小的内存为 108k

案例代码

public class Client {
    private static int stackLength = 1;

    public static void stackOverFlow() {
        stackLength++;
        stackOverFlow();
    }

    public static void main(String[] args) {
        try {
            stackOverFlow();
        }catch (Throwable t){
            System.out.println(stackLength);
            throw t;
        }
    }
}

测试结果

案例二、模拟单个栈帧过大, Java 虚拟机栈内存不足而导致的 StackOverflowError

在执行代码之前需要设置一下 Java 虚拟机栈的最大内存,对应的虚拟机参数如下

// 设置虚拟机栈最大内存空间为 128k
-Xss128k

案例代码

public class Client {
    private static int stackLength = 1;

    public static void stackOverFlow() {
        stackLength++;
        long d1,d2,d3,d4,d6,d7,d8,d9,d10,d11,d12,d13,d14,d15,d16,d17,d18,d19,d20,
                d21,d22,d23,d24,d25,d26,d27,d28,d29,d30,d31,d32,d33,d34,d35,d36,d37,d38,d39,d40,
                d41,d42,d43,d44,d45,d46,d47,d48,d49,d50,d51,d52,d53,d54,d55,d56,d57,d58,d59,d60,
                d61,d62,d63,d64,d65,d66,d67,d68,d69,d70,d71,d72,d73,d74,d75,d76,d77,d78,d79,d80,
                d81,d82,d83,d84,d85,d86,d87,d88,d89,d90,d91,d92,d93,d94,d95,d96,d97,d98,d99,d100;
        stackOverFlow();
    }

    public static void main(String[] args) {
        try {
            stackOverFlow();
        }catch (Throwable t){
            System.out.println(stackLength);
            throw t;
        }
    }
}

测试结果

案例一和案例二设置的 Java 虚拟机栈大小相同,案例一最大的栈帧数为 1090,而案例二的最大栈帧数仅仅为 53,这就证明了案例二中每一个栈帧的大小是比案例一中的栈帧要大的,之所以会这样是因为案例二中定义了很多的局部变量,从而导致每一个栈帧所占用的内存空间更大

这里需要特别注意的是,针对与我们常用的 hotspot 虚拟机,它的具体实现是不支持 Java 虚拟机栈动态扩展的,所以当虚拟机栈内存不足时只会出现 StackOverflowError,而不会出现 OutOfMemoryError

垃圾回收也不会针对与 Java 虚拟机栈和本地方法栈

 

相关面试题

举例虚拟机栈溢出的情况
在一个方法(栈帧)中定义过多的局部变量,导致单个栈帧所需要的内存空间大于 Java 虚拟机栈的最大空间(Xss 设置的值),就会 StackOverflowError
方法递归调用产生源源不断的栈帧,每个栈帧都有一定的大小,如果这些栈帧的总内存大小大于 Java 虚拟机栈的最大空间(Xss 设置的值),就会 StackOverflowError

调整虚拟机栈的大小,就能保证不出现 StackOverflowError 了吗
不会,只是有可能会延缓,例如方法的递归调用,如果没有设置递归结束的条件,就一定会爆出 StackOverflowError

分配的栈内存越大越好吗
Java 虚拟机栈大小可以通过 -Xss 参数进行设定,如果虚拟机栈设置的过小,容易出现 StackOverFlowError
由于操作系统的内存是有大小限制的,并且每一个线程都会有自己独立的 Java 虚拟机栈,如果每一个 Java 虚拟机栈设置的过大,那么可以并行的线程就会变少,从而影响多线程环境下的并发度

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

不会,每个方法被调用就对应着进栈的过程,方法调用完毕之后,栈帧就会被弹出虚拟机栈,不需要垃圾回收器进行回收

 

posted @ 2023-01-04 20:15  变体精灵  阅读(124)  评论(0编辑  收藏  举报