由浅入深了解JVM-虚拟机栈

虚拟机栈是JVM内存结构中线程私有的模块之一,特性是先进后出,这个特性决定了方法的调用过程都在栈中进行。每调用一个方法在栈中就会生成一个此方法对应的栈帧,栈帧中包括四个部分局部变量表、操作数栈、动态连接和方法返回地址。当一个栈的内存不足以容纳足够的栈帧时,即所谓的栈尝试大于虚拟所允许的深度时就会抛出StackOverFlowError异常。当对栈进行扩展遇到JVM内存不足时会抛出OutOfMemoryError异常。接下来通过图形和具体的代码对栈进行介绍

栈结构

栈的结构如下图所示:
在这里插入图片描述
其中Method1和Method2对应的是两个栈帧,从图中可知Method2中调用了Method1。每个栈帧中都有4个核心部分,分别为局部变量表、操作数栈、动态链接以及方法返回地址,当然也有其他部分,这里不作介绍。

局部变量表

见名知意其用于存放局变量的,即一个方法中的所有变量都存放在这种表里,在运行时阶段会针对每一个变量进行赋值。

操作数栈

用于存放方法中所有的基础类型变量的值,即局部变量表中变量所对应的具体的值。

动态链接

用于将常量池中指向某调用方法的符号引用转换为该调用方法的直接引用。

方法返回地址

可以理解成用于存放调用此方法的位置所在行号。例如:Method2 在第7行调用了Method1,那么Method1的栈帧中对应的方法返回地址就是7,当然可以这么去理解,但其实存放的是一段地址。

代码分析

通过上面的介绍,虽然有一个大致的了解,但可能还有一些模糊,接下来通过一段代码来分析。

public class JVMTest {
    
    
    public int add(){
    
    
        int a = 3;
        int b = 10;
        int c = a+b;
        return c;
    }
    public static void main(String[] args) {
    
    
        JVMTest jvmTest = new JVMTest();
        int result = jvmTest.add();
        System.out.println("The result is : "+result);
    }
}

这段代码很简单,一个类中两个方法,main 和 add 方法,其中在main方法中调用了add 方法,add方法执行了一个简单的加法运算。那么这段代码在运行时虚拟机栈到底是如何运作的,这里通过分析一下这段代码的字节码文件。字节码文件可以通过javap 命令翻译成可读的指令码。本代码通过 javap -c JVMTest.class 得到如下结果:

Compiled from "JVMTest.java"
public class com.research.JVMTest {
    
    
  public com.research.JVMTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int add();
    Code:
       0: iconst_3
       1: istore_1
       2: bipush        10
       4: istore_2
       5: iload_1
       6: iload_2
       7: iadd
       8: istore_3
       9: iload_3
      10: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/research/JVMTest
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method add:()I
      12: istore_2
      13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: iload_2
      17: invokedynamic #6,  0              // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
      22: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      25: return
}

看起来很头疼,慢慢来不着急,这里主要通过add方法和main方法来分析栈里四个核心区域是如何工作的。在分析前我们得读懂其中的指令,没必要记,读的时候通过官方的jvm指令手册查询即可。这里我将add方法中的10条指令整理如下:

指令 作用 作用域
iconst_3 int 型常量值3进操作数栈 操作数栈
istore_1 将栈顶int型数值存入第一个局部变量 从操作数栈到局部变量表
bipush 将单字节的常量值(-128~127)推送至栈顶 操作数栈
istore_2 将栈顶int型数值存入第二个本地变量 从操作数栈到局部变量表
iload_1 将第一个int型本地变量推送至操作数栈顶 从局部变量表到操作数栈
iload_2 将第二个int型本地变量推送至操作数栈顶 从局部变量表到操作数栈
iadd 将栈顶两int型数值相加并将结果压入操作数栈顶 操作数栈
istore_3 将栈顶int型数值存入第三个本地变量 从操作数栈到局部变量表
iload_3 将第三个int型本地变量推送至操作数栈顶 从局部变量表到操作数栈
ireturn 从当前方法返回int 操作数栈

PS: 大学学过汇编的同学看到这里应该会觉得这些指令似曾相识,当然我也是这么觉得的
整个add方法的执行过程如下图所示,每一大块代表一条指令,指令中显示了栈帧中各个部分的动态变化,其中局部变量表和操作数栈都对应着一块内存,操作数栈说白了就是一个暂存区,暂存变量和计算的中结果,方法返回地址作为此方法的出口。
在这里插入图片描述
不难发现add方法中定义的都是基本类型变量,如果定义的是引用类型变量,局部变量表中会是一种什么情况。看向main方法:
main方法中定义了一个引用类型的局部变量,它的值是一个指向对象的地址,当然这个对象不是存放在操作数栈中,而是存放在堆中(由浅入深了解JVM-堆)。写到这里可以在虚拟机栈和堆之间建立一座桥了:
在这里插入图片描述
快看到光明了,剩下最后一个核心部分:动态链接。既然说是动态链接,它是怎么个动态法。
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( symbolic Reference)保存在class文件的常量池(方法区)里。 比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的(上文字节码main方法中:17: invokedynamic #6, 0 ),那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

至此,虚拟栈的工作原理介绍完了,至于其他部分的工作原理请参考如下博文
由浅入深了解JVM-内存结构
由浅入深了解JVM-堆
由浅入深了解JVM-程序计数器

猜你喜欢

转载自blog.csdn.net/hongyinanhai00/article/details/113856144