暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

​拆解栈帧中本地变量表

一十二章经 2020-12-10
1716
JVM内存模型中虚拟机栈(JVM Stacks)是重要的组成部分。虽然虚拟机栈不需要GC,但也是性能优化部分中很重要的一块区域,在这块区域中我们主要的优化内容其实是本地变量表(LocalVariableTable),本篇主要介绍虚拟机栈在内存中的结构与本地变量表的作用。

虚拟机栈

首先明确一点,操作系统中的函数方法的调用像是一个倒立的兵乓球筒,每一次方法的入栈像是从下往上塞入一个兵乓球,一个方法是一个栈帧也就是一个兵乓球

从上图中可以看到,默认系统给栈分配的内存是固定的(也有动态扩展的方式)。在一层层压栈之后,栈顶的内存地址是在逐渐变小而不是变大。当栈帧过多或者栈帧过大导致栈内存超出了系统分配的内存时就会产生“Stack Over Flow”也就是栈溢出错误

虽然JVM的内存模型存在于操作系统的中,但我们可以将JVM中堆类比成操作系统的堆,将JVM的栈类比成操作系统的栈的

虚拟机栈的入栈出栈方式跟OS栈的出入栈方式是一样的,都是栈底的内存地址先固定,然后随着不断的压入栈帧,栈空间不断减少。在虚拟机规范中有这样一段描述,如果线程所需的虚拟机栈的容量大于系统分配的大小则随着栈帧不断的被压入,会产生StackOverflowError。如果虚拟机栈的容量是可动态扩展的,当栈空间满时,系统会申请一个更多内存空间的栈用于执行当前线程,如果申请不到则会抛出OutOfMemoryError

在HotSpot虚拟机中,默认是采用不可扩展的虚拟机栈,即当栈满后抛出StackOverflowError。另外,在64位Linux操作系统中,JDK1.8的默认虚拟机栈大小是1024k

# 终端命令行查看虚拟机栈大小
java -XX:+PrintFlagsFinal -version | grep -i 'stack'

知道虚拟机栈在OS的分布情况后再继续看看栈中详细的组成部分。

栈帧

Java程序中的方法调用跟操作系统层面的OS调用是一致的。我们先理解一下OS中的函数调用过程,在函数的调用中有两个非常重要的寄存器:rsp栈指针(Stack Pointer)、rbp栈帧指针(Frame Pointer)。rsp永远指向系统栈最上面一个栈帧的栈顶,rbp永远指向系统栈最上面一个栈帧的底部,也可以认为是存放了当前栈帧位置的寄存器

入栈过程

rbp与rsp指针的值会随着入栈跟弹栈而改变。入栈的过程是根据PC寄存器下一条指令的地址压入栈中,此时rbp的值会变成刚入栈的栈帧的起始位置,也就是当前栈帧底,然后rsp会更新至栈顶,也就是当前栈帧的顶部

出栈过程

PC寄存器的指令不断被执行后,整个栈帧执行完毕需要出栈时,会产生一条 pop rbp的指令,这条指令就可以将当前栈帧出栈。在出栈后还必须调用return指令,将程序的执行控制权返回到出栈后的栈顶,也就是图中的栈帧2

通过rbp与rsp也就实现了栈的压栈与出栈。OS的出入栈方式在JVM中同样也使用

局部变量表

在了解本地变量表时我们需要反编译.class文件,将.class文件反编译成字节码文件需要借助两个工具,javap指令或者idea的【jclasslib】插件,二者选其一即可

测试代码如下

public class Test02LocalVariables {
    public static void main(String[] args) {
        Test02LocalVariables variablesTable = new Test02LocalVariables();
        int num = 10;
        variablesTable.test3();
    }

    public static void test1(String str1) {
        int k = 10;
    }

    public static String test2(String str1, String str2) {
        int k = 10;
        return "";
    }

    public void test3() {
        long q = 100L;
        int a = 10;
    }

    public void test4() {
        int a = 0;
        {
            int j = 0;
            j = a + 1;
        }
        int c = a + 1;
    }
}

利用插件jclasslib可以看到方法栏中有6个方法:

其中 [0]为构造方法,里面的本地变量只有一个this。

先点开[1] main方法查看LocalVariableTable

由此可见方法的参数和局部变量都会加载进局部变量表中,另外我们还必须认识到局部变量表是一个数组。图中第二列【起始位置PC】中的值对应的是反编译后的字节码的行号,如下所示

【长度】列的数字是该变量的作用范围,意思就是说从【起始PC】往后加上多少个【长度】就是该变量的作用范围。这里有一个很有意思的地方,就是每一行的【起始PC】+【长度】都等于16。这是因为该栈帧中所有的指令行号总共就16,请看上图中的0~15的行号

【序号】也叫slot插槽,其本质也是一个数组,用于标记存放局部变量的存储空间大小。除了double、long类型占用两个slot外,其实所有类型都只占用一个slot,查看代码中的test3()栈帧可知

q变量占用了slot1、2,a变量只能占用slot3

【名字】是程序员赋予该变量的名字,【描述符】表示该变量的类型。在上图中还可以发现有一个this变量,这是因为只要不是static修改的方法都会有一个this本地变量并且占据第一个slot。从这里我们也可以解释为什么静态方法中无法使用this关键字。另外,局部变量表的大小是在Java编译时就已经确定下来,程序运行时也不会发生动态的变化

开头提及过,在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表,这是因为局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。


文章转载自一十二章经,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论