个人名片:

【实战JVM】Java虚拟机栈_jvm


🐼作者简介:一名大三在校生,喜欢AI编程🎋
🐻❄️个人主页🥇:落798. 🐼

  • RabbitMQ快速入门🔥 🐓每日一句:🍭我很忙,但我要忙的有意义!
    欢迎评论 💬点赞👍🏻 收藏 📂加关注+



文章目录

  • Java虚拟机栈
  • 局部变量表
  • 操作数栈
  • 帧数据


【实战JVM】Java虚拟机栈_Java_02

Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack)采用栈的数据结构来管理方法调用中的基本数据,先进后出(First In Last Out),每一个方法的调用使用一个栈帧(Stack Frame)来保存。

public class MethodDemo {   
    public static void main(String[] args) {        
         study();    
     }

    public static void study(){
        eat();

        sleep();
    }   
    
    public static void eat(){       
         System.out.println("吃饭");   
    }    
    
    public static void sleep(){        
        System.out.println("睡觉");    
        }
  }

main方法执行时,会创建main方法的栈帧:

【实战JVM】Java虚拟机栈_局部变量_03


接下来执行study方法,会创建study方法的栈帧

【实战JVM】Java虚拟机栈_Java_04

进入eat方法,创建eat方法的栈帧

【实战JVM】Java虚拟机栈_jvm_05

eat方法执行完之后,会弹出它的栈帧:

【实战JVM】Java虚拟机栈_Java_06

然后调用sleep方法,创建sleep方法栈帧

【实战JVM】Java虚拟机栈_操作数_07

最后study方法结束之后弹出栈帧,main方法结束之后弹出main的栈帧。

在IDEA中也可以看到对应的栈帧:

package chapter03.frame;

/**
 * 栈帧测试1
 */
public class FrameDemo {
    public static void main(String[] args) {
        A();
    }

    public static void A() {
        System.out.println("A执行了...");
        B();
    }

    public static void B() {
        System.out.println("B执行了...");
        C();
    }

    public static void C() {
        System.out.println("C执行了...");
        throw new RuntimeException("测试");
    }
}

打上断点debug之后会出现栈帧内容:

【实战JVM】Java虚拟机栈_操作数_08

Java虚拟机栈随着线程的创建而创建,而回收则会在线程的销毁时进行。由于方法可能会在不同线程中执行,每个线程都会包含一个自己的虚拟机栈。如下就有两个线程的虚拟机栈,main线程和线程A。

【实战JVM】Java虚拟机栈_字节码_09


Java虚拟机栈的栈帧中主要包含三方面的内容:

  • 局部变量表,局部变量表的作用是在运行过程中存放所有的局部变量
  • 操作数栈,操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域
  • 帧数据,帧数据主要包含动态链接、方法出口、异常表的引用

局部变量表

局部变量表的作用是在方法执行过程中存放所有的局部变量。局部变量表分为两种,一种是字节码文件中的,另外一种是栈帧中的也就是保存在内存中。栈帧中的局部变量表是根据字节码文件中的内容生成的。

我们先来看下字节码文件中的局部变量表:编译成字节码文件时就可以确定局部变量表的内容。

public static void test1(){
    int i = 0;
    long j = 1;
}

test1方法的局部变量表如下:

【实战JVM】Java虚拟机栈_操作数_10


局部变量表中保存了字节码指令生效的偏移量:

【实战JVM】Java虚拟机栈_局部变量_11


比如i这个变量,它的起始PC是2,代表从lconst_1这句指令开始才能使用i,长度为3,也就是2-4这三句指令都可以使用i。为什么从2才能使用,因为0和1这两句字节码指令还在处理int i = 0这句赋值语句。j这个变量只有等3指令执行完之后也就是long j = 1代码执行完之后才能使用,所以起始PC为4,只能在4这行字节码指令中使用。

接下来看下栈帧中的局部变量表,栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot) ,long和double类型占用两个槽,其他类型占用一个槽。

【实战JVM】Java虚拟机栈_局部变量_12


i占用数组下标为0的位置,j占用数组下标1-2的位置。

刚才看到的是静态方法,实例方法中的序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存中存放实例对象的地址。

【实战JVM】Java虚拟机栈_字节码_13


方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致。局部变量表保存的内容有:实例方法的this对象,方法的参数,方法体中声明的局部变量。

【实战JVM】Java虚拟机栈_操作数_14


test3方法中包含两个参数k,m,这两个参数也会被加入到局部变量表中。

以下代码的局部变量表中会占用几个槽?

public void test4(int k,int m){
    {
        int a = 1;
        int b = 2;
    }
    {
        int c = 1;
    }
    int i = 0;
    long j = 1;
}

为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用。

1、方法执行时,实例对象thiskm 会被放入局部变量表中,占用3个槽

【实战JVM】Java虚拟机栈_字节码_15


2、将1的值放入局部变量表下标为3的位置上,相当于给a进行赋值。

【实战JVM】Java虚拟机栈_字节码_16


3、将2放入局部变量表下标为4的位置,给b赋值为2。

【实战JVM】Java虚拟机栈_jvm_17


4、ab已经脱离了生效范围,所以下标为3和4的这两个位置可以复用。此时c的值1就可以放入下标为3的位置。

【实战JVM】Java虚拟机栈_局部变量_18


4、脱离c的生效范围之后,给i赋值就可以复用c的位置。

【实战JVM】Java虚拟机栈_操作数_19


5、最后放入jj是一个long类型,占用两个槽。但是可以复用b所在的位置,所以占用4和5这两个位置

【实战JVM】Java虚拟机栈_字节码_20


所以,局部变量表数值的长度为6。这一点在编译期间就可以确定了,运行过程中只需要在栈帧中创建长度为6的数组即可。

【实战JVM】Java虚拟机栈_局部变量_21

操作数栈

操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。

在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小。

【实战JVM】Java虚拟机栈_jvm_22


比如之前的相加案例中,操作数栈最大的深入会出现在这个时刻:

【实战JVM】Java虚拟机栈_字节码_23


所以操作数栈的深度会定义为2。

帧数据

帧数据主要包含动态链接、方法出口、异常表的引用。

动态链接
当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。

【实战JVM】Java虚拟机栈_Java_24


方法出口

方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址。

【实战JVM】Java虚拟机栈_字节码_25


异常表

异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置。

【实战JVM】Java虚拟机栈_jvm_26


如下案例:i=1这行源代码编译成字节码指令之后,会包含偏移量2-4这三行指令。其中2-3是对i进行赋值1的操作,4的没有异常就跳转到10方法结束。如果出现异常的情况下,继续执行到7这行指令,7会将异常对象放入操作数栈中,这样在catch代码块中就可以使用异常对象了。接下来执行8-9,对i进行赋值为2的操作。

【实战JVM】Java虚拟机栈_jvm_27

所以异常表中,异常捕获的起始偏移量就是2,结束偏移量是4,在2-4执行过程中抛出了java.lang.Exception对象或者子类对象,就会将其捕获,然后跳转到偏移量为7的指令。

栈内存溢出
Java虚拟机栈如果栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现内存溢出。Java虚拟机栈内存溢出时会出现StackOverflowError的错误。

【实战JVM】Java虚拟机栈_字节码_28

如果我们不指定栈的大小,JVM 将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构。

【实战JVM】Java虚拟机栈_字节码_29


我们来模拟下栈内存的溢出情况:

public static int count = 0;
     //递归方法调用自己
     public static void recursion(){
         System.out.println(++count);
         recursion();
     }

使用递归让方法调用自身,但是不设置退出条件。定义调用次数的变量,每一次调用让变量加1。查看错误发生时总调用的次数。

【实战JVM】Java虚拟机栈_操作数_30


执行之后可以打印出溢出时总栈帧的数量,并且发现虚拟机已经抛出了StackOverflow的错误。

要修改Java虚拟机栈的大小,可以使用虚拟机参数 -Xss 。

  • 语法:-Xss栈大小
  • 单位:字节(默认,必须是 1024 的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)
    例如:
-Xss1048576 
-Xss1024K      
-Xss1m
-Xss1g

操作步骤如下,不同IDEA版本的设置方式会略有不同:
1、点击修改配置Modify options
2、点击Add VM options
3、添加参数

【实战JVM】Java虚拟机栈_操作数_31


调成512k之后,明显发现最大栈帧数量减少了:

【实战JVM】Java虚拟机栈_字节码_32

注意事项:
1、与-Xss类似,也可以使用 -XX:ThreadStackSize 调整标志来配置堆栈大小。
格式为: -XX:ThreadStackSize=1024

2、HotSpot JVM对栈大小的最大值和最小值有要求:
比如测试如下两个参数,会直接报错:
-Xss1k -Xss1025m Windows(64位)下的JDK8测试最小值为180k,最大值为1024m。

3、局部变量过多、操作数栈深度过大也会影响栈内存的大小。我们在这段代码中添加一些局部变量。

//递归方法调用自己
public static void recursion() {
    long a,b,c,d,f,g,h,i,j,k;
    System.out.println(++count);
    recursion();
}

使用默认大小来测试之后,发现栈帧数量从10000+减少了到8000+

【实战JVM】Java虚拟机栈_局部变量_33


一般情况下,工作中即便使用了递归进行操作,栈的深度最多也只能到几百,不会出现栈的溢出。所以此参数可以手动指定为-Xss256k节省内存。


欢迎评论 💬点赞👍🏻 收藏 📂加关注+