美文网首页一些收藏
JVM那些事儿-栈内存解析(二)

JVM那些事儿-栈内存解析(二)

作者: 久伴我还是酒伴我 | 来源:发表于2022-03-17 17:30 被阅读0次

简介

每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈和出栈;
帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。

栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现;

线程中的方法调用链可能会很长,每个方法都会生成对应的一块栈帧空间,方法以两种方式完成,一种通过return返回的,称为正常返回;一种是通过抛出异常而异常终止的。不管以哪种方式返回,虚拟机都会将当前帧弹出Java栈然后释放掉,这样上一个方法的帧就成为当前帧了。

栈帧的结构

image.png

局部变量表(Local Variable Table)

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。

局部变量表的容量以变量槽(Slot)为最小单位,虚拟机规范中未说明它该有多大,只说每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,reference类型表示对一个对象实例的引用,虚拟机对它的长度和结构没有说明。

为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量其作用域并不一定会覆盖整个方法体,如果当前字节码程序计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。不过这样的设计虽节省了空间,但也会有一定的副作用,例如在某些情况下,Slot的复用会直接影响到系统的垃圾收集行为。

说了那么多,其实就把它理解为存储当前方法内的局部变量的。

操作数栈(Operand Stack)

操作数栈也常被称为操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也是编译的时候被写入到方法表的Code属性的 max_stacks数据项中。操作数栈的每一个元素可以是任意Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位 数据类型所占的栈容量为2。栈容量的单位为“字宽”,对于32位虚拟机来说,一个”字宽“占4个字节,对于64位虚拟机来说,一个”字宽“占8个字节。

当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。

另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下 栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了。

我们可以通过一段代码看下

public class Match {
   public static final int initData = 666;
    public static Sat sat = new Sat();
    public byte[] arr = new byte[1024 * 25];

    public int compute(){
        int a = 1;
        int b = 2;
        int c = (a + b) * 100;
        return c;
    }

    public static void main(String[] args) throws Exception {
        Match math = new Match();
        int result = math.compute();
        System.out.println(result);
    }
}

如果我们执行上述代码,此线程开始到此线程结束,共执行了两个方法,该线程对应的也就是两个栈帧,同数据结构栈一致,也是遵循先进后出的方式进行栈帧的加载。


image.png
image.png

通过字节码反汇编后

Compiled from "Match.java"
public class com.kxl.e.invoice.admin.utils.Match {
  public static final int initData;

  public static com.kxl.e.invoice.admin.biz.cert.dao.entity.Sat sat;

  public byte[] arr;

  public com.kxl.e.invoice.admin.utils.Match();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: sipush        25600
       8: newarray       byte
      10: putfield      #2                  // Field arr:[B
      13: return

  public int compute();
    Code:
       0: iconst_1  //iconst_1 将int类型常量1压入栈
       1: istore_1  //istore_1 将int类型值存入局部变量1
       2: iconst_2 //iconst_2 将int类型常量2压入栈
       3: istore_2 //istore_2 将int类型值存入局部变量2
       4: iload_1 //iload_1 从局部变量1中装载int类型值
       5: iload_2 //iload_2 从局部变量2中装载int类型值
       6: iadd //iadd 执行int类型的加法
       7: bipush        100 //bipush 将一个8位带符号整数压入栈
       9: imul //mul 执行int类型的乘法
      10: istore_3 //istore_3 将int类型值存入局部变量3
      11: iload_3 //iload_3 从局部变量3中装载int类型值
      12: ireturn //ireturn 从方法中返回int类型的数据

 public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: new           #3                  // class com/kxl/e/invoice/admin/utils/Match
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #5                  // Method compute:()I
      12: istore_2
      13: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: iload_2
      17: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
      20: return

  static {};
    Code:
       0: new           #8                  // class com/kxl/e/invoice/admin/biz/cert/dao/entity/Sat
       3: dup
       4: invokespecial #9                  // Method com/kxl/e/invoice/admin/biz/cert/dao/entity/Sat."<init>":()V
       7: putstatic     #10                 // Field sat:Lcom/kxl/e/invoice/admin/biz/cert/dao/entity/Sat;
      10: return
}

具体细节可以参照“JVM指令手册”
地址:https://www.jianshu.com/p/53a052adffc1

操作架构图:

0: iconst_1  //iconst_1 将int类型常量1压入栈
1: istore_1  //istore_1 将int类型值存入局部变量1
image.png
4: iload_1 //iload_1 从局部变量1中装载int类型值       
5: iload_2 //iload_2 从局部变量2中装载int类型值
image.png
  6: iadd //iadd 执行int类型的加法
image.png
image.png
 7: bipush        100 //bipush 将一个8位带符号整数压入栈
image.png
 9: imul //mul 执行int类型的乘法
image.png
image.png
10: istore_3 //istore_3 将int类型值存入局部变量3
image.png

动态链接

符号引用和直接引用在运行时进行**解析和链接的过程,叫动态链接
一个方法调用另一个方法,或者一个类使用另一个类的成员变量时,需要知道其名字
符号引用就相当于名字,这些被调用者的名字就存放在Java字节码文件里(.class 文件)
名字知道了,但是Java真正运行起来的时候,如何靠这个名字(符号引用)找到相应的类和方法
需要解析成相应的直接引用,利用直接引用来准确地找到。

方法出口

image.png

当代码执行到26行的时候,进入compute方法之前执行该指令,将 compute方法执行完毕之后执行的代码行号保存到 compute方法对应" 栈帧 " 中的方法出口中,compute方法的 " 方法出口 " 是第 26 行代码!

帧数据区

帧数据区的大小依赖于 JVM 的具体实现

程序计数器

image.png
image.png

指向当前线程所执行的字节码指令(地址)行号。
程序计数器由字节码执行引擎控制操作。
注意:为什么字节码中没有行数8 ,其实100 对应的就是8,只不过是一个隐形的行数

本地方法栈

本地方法栈和虚拟机栈所发挥的作用时非常相似的,虚拟机栈为虚拟机执行Java方法服务,本地方法栈则为虚拟机使用到Native方法服务,虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。

Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。

代码案例

image.png image.png

相关文章

网友评论

    本文标题:JVM那些事儿-栈内存解析(二)

    本文链接:https://www.haomeiwen.com/subject/sxhvdrtx.html