JAVA

Java虚拟机栈与栈帧详解

前言


栈与堆的区别

栈是运行时的单位,而堆是存储的单位。

即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

什么是堆?

堆内存是java内存中的一种,它的作用是用于存储java中的对象和数组,当我们new一个对象或者创建一个数组的时候,就会在堆内存中开辟一段空间给它,用于存放。

用new创建的对象在堆区,函数中的临时变量在栈区,Java中的字符串在字符串常量区

JVM启动后堆内存会随之创建。堆内存是多个线程共享的。

Java中有一个非常好的特性,自动垃圾回收。垃圾回收会操作这个数据区来回收对象从而释放内存。但如果堆内存剩余的内存不足以满足于对象创建时,JVM虚拟机就会抛出OutOfMemoryError错误。

什么是栈?

栈内存是Java的另一种内存,主要是用来执行程序用的,比如:基本类型的变量和对象的引用变量
栈内存的特点:栈内存就好像一个矿泉水瓶,往里面放入东西,那马先放入的沉入底部,所以它的特点是:先进后出,后进先,栈内存由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

栈内存存放进本数据类型的数据和对象的引用,但对象本身不存在栈中,而是存放在堆中。

举个不太恰当的例子解释


可以把堆理解为一家餐厅,里面有200张桌子,也就是最多能同时容纳200桌客人就餐,来一批客人就为他们安排一些桌子,如果某天来的客人特别多,超过200桌了,那就不能再接待超出的客人了。

当然,进来吃饭的客人不可能是同时的,有的早,有的晚,先吃好的客人,老板会安排给他们结账走人,然后空出来的桌子又能接待新的客人。

这里,堆就是餐厅,最大容量200桌就是堆内存的大小,老板就相当于GC(垃圾回收),给客人安排桌子就相当于java创建对象的时候分配堆内存,结账就相当于GC回收对象占用的空间。

接着把栈比作一口废井,这口井多年不用已经没水了,主人现在把它作为贮存自酿酒的地方,存酒的时候就用绳子勾着酒坛子慢慢放下去,后面再存就一坛一坛堆着放上去,取酒的时候就先取最上面的坛子。

Java堆区和栈区的区别


堆区(Heap):

堆唯一的目的就是用于存放对象实例,每个Java应用都唯一对应一个JVM实例,每个JVM实例都唯一对应一个堆,该堆对应的堆内存被应用的所有线程共享。

栈区:

每个线程包含一个栈区,占中只保存基础数据类型的对象和自定义对象引用(不是对象),对象都存放在堆区中,每个栈区中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。

  • 栈内存是线程私有的
  • 堆内存是所有线程共有的

Java虚拟机栈详细介绍


Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,是线程私有的。

JVM是基于堆栈的虚拟机,JVM为新创建的线程都分配一个堆栈,也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。

Java栈用于保存方法调用的状态和局部变量。例如,当方法被调用时,其传递的参数和在该方法中声明的变量就会被推入 Java 栈中。当方法返回时,它们将从栈中弹出。每个线程都有自己的 Java 栈,因此局部变量对于不同的线程是相互独立的。

生命周期

Java栈也是每个线程私有的一块内存区域,它的生命周期和线程的生命周期一样长。

每个线程在创建时都会新建一个相应的栈帧,用于存放该线程的局部变量表、操作数栈、基本数据类型等信息。

方法的调用就是通过栈来完成的,每次方法调用将会压入一个新的栈帧,并在该栈帧中存放当前方法的参数、局部变量等信息。

在方法调用结束后,对应的栈帧将会被弹出,同时这些数据也就随之消失。

作用

主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的 调用和返回。

栈的特点

  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
  • JVM直接对Java栈的操作只有两个: ①每个方法执行,伴随着进栈(入栈、压栈)②执行结束后的出栈工作
  • 对于栈来说不存在垃圾回收问题

栈的存储单位

  • 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

栈运行原理

  • JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame), 与当前栈帧相对应的方法就是当前方法(Current Method), 定义这个方法的类就是当前类(Current class)
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。

不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。 – 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出

栈桢的内部结构


  • 每个栈帧中存储着:
  • 局部变量表(Local Variables)
  • 操作数栈(operand stack)(或表达式栈)
  • 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
  • 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
  • 一些附加信息

如图,我们可以看到,在Java栈中存放的都是一个个的栈帧,而每个栈帧又对应着一个被调用的方法,在栈帧中,包含着局部变量表、指向当前方法所属的类的运行时常量池、操作数栈、方法返回地址与一些额外附加信息。

每当线程执行一个方法时,它就会跟着创建一个对应栈帧,并把建立的栈帧压栈。方法执行完毕后,再把栈帧出栈。

由此我们可知,线程与当前所执行的方法对应的栈帧是必定位于Java栈顶部的。因此对于所有的程序设计语言而言,栈这部分空间对开发者来说是不透明的。

栈内存的大小是有两种设置,它的固定值和根据线程需要动态增长。

在JVM栈 这个数据区可能会发生抛出两种错误。

  • StackOverflowError 出现在栈内存设置成固定值的时候,当程序执行需要的栈内存超过设定的固定值会抛出这个错误。
  • OutOfMemoryError 出现在栈内存设置成动态增长的时候,当JVM尝试申请的内存大小超过了其可用内存时会抛出这个错误。

一个简单的 Java 代码来理解一下


public class Example {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        String s1 = "hello";
        String s2 = "world";
        System.out.println
    }
}

在执行该程序时,Java 虚拟机会为每个线程创建一个 Java 栈,并分配相应的内存空间。在本例中,主线程 main 的 Java 栈将包含以下内容:

1. 压入 main 方法帧

2. 在帧中声明了四个局部变量 a、b、s1 和 s2,将它们分别初始化为 10、20、"hello" 和 "world"

3. 将对象引用s1 和 s2 压入栈中

4. 执行字符串连接操作,并将结果压回栈中

5. 调用 System.out.println() 方法,打印传递给它的参数

6. 弹出所有数据,销毁当前帧

另一方面,Java 堆中将包含两个字符串对象。当我们创建 String 对象时,Java 虚拟机会在堆上为其分配内存,并将其地址赋值给相应的引用变量。例如,在本例中,变量 s1 和 s2 引用的字符串对象都被分配到了 Java 堆上。

总结


Java堆和栈是Java虚拟机内存管理的两个重要部分,它们分别负责管理Java对象和线程调用栈。

Java堆是用来存储Java对象的内存区域。在Java应用程序中,所有的对象都是在堆上分配的。Java堆是被所有线程共享的,当Java虚拟机启动时,会在堆中分配一块固定大小的内存区域,称为初始堆,堆的大小可以通过命令行参数或配置文件进行配置。当Java程序需要创建对象时,Java虚拟机会在堆中分配一块足够大小的内存空间,用来存储新对象的实例变量和其他数据。Java堆还负责垃圾回收,当Java虚拟机检测到某个对象不再被引用时,会自动回收该对象所占用的内存空间。

Java栈是用来存储线程调用栈的内存区域。在Java应用程序中,每个线程都有一个独立的Java栈。Java栈是线程私有的,当线程调用一个方法时,Java虚拟机会在该线程的栈中分配一块内存空间,用来存储方法的局部变量和操作数栈等数据。当方法返回时,Java虚拟机会弹出栈顶元素,并释放该元素所占用的内存空间。Java栈还负责线程的异常处理,当线程抛出异常时,Java虚拟机会在栈中查找适当的异常处理器,并执行相应的异常处理代码。

综上所述,Java堆和栈分别负责管理Java对象和线程调用栈。Java堆用来存储Java对象的实例变量和其他数据,并负责垃圾回收。Java栈用来存储方法的局部变量和操作数栈等数据,并负责线程的异常处理。在Java应用程序中,Java堆和栈是非常重要的内存区域,对程序的性能和稳定性有着重要的影响。