首页 > 其他分享 >JVM--虚拟机栈

JVM--虚拟机栈

时间:2023-01-04 20:22:59浏览次数:44  
标签:操作数 Java -- 虚拟机 局部变量 JVM main 栈帧

一、简介

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 虚拟机栈,如果每一个 Java 虚拟机栈设置的过大,那么可以并行的线程就会变少,从而影响多线程环境下的并发度

 

标签:操作数,Java,--,虚拟机,局部变量,JVM,main,栈帧
From: https://www.cnblogs.com/xiaomaomao/p/17025908.html

相关文章

  • 10-11代码笔记
    1.日期格式FormatDateTime函数详解c以短时间格式显示时间,即全部是数字的表示FormatdateTime('c',now);输出为:2004-8-79:55:40d对应于时间中的日期,日期是一位则显......
  • STM32串口代码
    介绍在usart.c中进行了:usart的初始化发送字符串函数的编写printf和scanf的C库重定向中断中根据STM32接受到的信息进行开灯和关灯操作在led.c中进行了:led的GPIO的......
  • OI是什么?
    从OI谈起提到OI,也许很多人并不清楚这是怎么一回事。对于在学校就学习过数学、物理、化学和生物的同学们来说,“国际五项学科奥林匹克竞赛”中的这四门是相当熟悉了(相对OI来......
  • redisson连接错误 Unable to init enough connections amount Only 23 from 32 were i
    背景开发过程中遇到了这个问题,翻找了一些帖子,记录一些“可能”的解决方案。出现问题的原因可能各有不同--redis官方回复是网络问题可选择的解决方案:--将redis连接超......
  • 软件工程相关
    什么是面向对象分析?其主要思想是什么?面向对象方法是一种运用对象、类、封装、继承、多态和消息等概念来构造、测试、重构软件的方法。思想:面向对象方法从对象出发,发展出......
  • rpm使用教程
    rpm选项住选择-i安装-e卸载-U升级-q查找辅助选项-v显示过程-h--bash查询-a--all查询所有安装的包-f--file查询拥有<--file>的包-p查询......
  • 重新编译gluster_exporter源码,构建镜像
    1.githubhttps://github.com/ofesseler/gluster_exporter 2.dockerfileFROMgolang:1.17ENVGO111MODULE=on\GOPROXY="https://goproxy.cn,direct"COPYglus......
  • Dos命令以及特性和版本
    #基本的Dos命令 ##打开CMD的方式 1.开始+系统+命令提示符2.win键+R输入CMD打开控制台3.在任意文件夹下面,按住Shift键+鼠标右击,在此处打开命令窗口4.资源管理......
  • vite+vue3使用transition
    一番操作发现切换路由竟然没效果,控制台打印了警告原因是确实根节点,按照如下方式解决,可以愉快的进行路由切换了......
  • 利用GUI制作拼图小游戏
    JFrame表示窗体JMenuBar表示菜单,JMenu表示菜单中的字,JMenuitem表示目录JLabel表示管理文字和图片的文字JFrame,JMenuBar,JLabel称为组件利用空参构造对对象进行初始化:pu......