一、概述
1、概念
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。它是线程私有的。
2、生命周期
生命周期和线程的一致。随着线程的创建而创建,销毁而销毁。
3、作用
主管Java程序的运行。它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
4、栈和堆的区别
栈是运行时的单位,堆是存储的单位
栈解决的是程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪
5、栈的特点
- 栈是一种快速有效的分配储存方式,访问速度仅次于程序计数器
- JVM直接对Java栈的操作只有进栈和出栈操作
- 对于栈来说不存垃圾回收问题
6、栈可能出现的异常
JVM规范允许Java栈的大小是动态的或者是固定不变的
- 如果是固定不变的,当超出栈的最大容量时,就会抛出 StackOverflowError 异常
- 如果是动态扩展的,在尝试宽展时无法申请到足够的内存(JVM的内存不够时,自然无法申请到内存),就会抛出 OutOfMemoryError 异常。即OOM
- 可以通过 -Xss 来设置栈的大小。如 -Xss 128m
二、栈的存储单位
1、栈帧
- 每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在
- 在这个线程上正在执行的每个方法都各自对用一个栈帧
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
2、栈帧的内部结构
- 局部变量表
- 操作数栈
- 方法返回地址
- 动态链接
- 一些附加信息
如图所示:
3、局部变量表
- 本质是一个数组,主要用于存储方法参数和定义在方法体内的局部变量
- 线程私有,不存在数据安全问题
- 局部变量表的容量大小是在编译期确定下来的,在方法运行期间不会改变大小
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套的次数据越多。
- 局部变量表中的变量只在当前方法调用中有效。在方法的运行期间,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着栈帧的销毁,局部变量表也会随之销毁
补充说明:
在方法中的类变量可以没有初始值,因为在类加载的时候会为类变量设置零值,并初始值。但是局部变量不会被设置初始值,所以方法中定义的局部变量必须显示的设置初始值。
如图:
没有为变量 i 设置初始值,在IDEA中直接就报错了。
4、操作数栈
底层数组实现。在方法的执行过程中,根据字节码指令,向栈中写入数据或提取数据,即入栈/出栈。。
- 操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
- 它是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧会被创建出来,这时,这个方法的操作数栈是空的
- 操作数栈的大小在编译时期就确定好了
- 因为采用的是栈的数据结果,所有不能通过索引来访问数据,只能通过入栈和出栈来操作数据
代码追踪:
编写如下代码
public class OperandStackTest {
public void test() {
int i = 10;
int j = 20;
int k = i + j;
}
}
对应的字节码:
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
程序编译完成时,PC寄存器、局部变量表、操作数栈都为空。并且局部变量表的大小已经确定为3(应为就3个变量 i、j、k),操作数栈的大小为2。如下图所示:
(1)程序执行第 1 行:
当程序执行第 1 行字节码时,PC寄存器记录当前操作的指令地址 0,执行 bipush 操作,把 10 压入操作数栈。
(2)程序执行第 2 行:
执行第 2 行字节码时,PC寄存器记录当前操作的指令地址 2,执行 istore_1 操作,把 10 从操作数栈中取出来,存入索引为 1 的局部变量表中。
补充说明:局部变量表的索引是从 0 开始的。因为该方法不是静态方法,所以索引为 0 的位置放的是 this 对象 的引用 (偷个懒就没有画出来)
(3)执行第 3 行代码
执行第 3 行字节码时,PC寄存器记录当前操作的指令地址 3,执行 bipush 操作,把 20 压入操作数栈。
(4)执行第 4 行代码
执行第 4 行字节码时,PC寄存器记录当前操作的指令地址 5,执行 istore_2 操作,把 20 从操作数栈中取出来,存入索引为 2 的局部变量表中。
(5)执行第 5 行代码
执行第 5 行字节码时,PC寄存器记录当前操作的指令地址 6,执行 iload_1 操作,把局部变量表中索引为 1 的数据取出来,压入操作数栈
(6)执行第 6 行代码
执行第 6 行字节码时,PC寄存器记录当前操作的指令地址 7,执行 iload_2 操作,把局部变量表中索引为 2 的数据取出来,压入操作数栈
(7)执行第 7 行代码
执行第 7 行字节码时,PC寄存器记录当前操作的指令地址 8 ,执行 iadd 操作,把局部变量表中的数据取出来,把计算结果存入操作数栈(执行引擎会把字节码翻译成机器指令交由CPU计算)
(8)执行第 8 行代码
执行第 8 行字节码时,PC寄存器记录当前操作的指令地址 9,执行 istore_3 操作,把 30 从操作数栈中取出来,存入索引为 3 的局部变量表中。
最后执行 return 操作
5、动态链接
也可以叫做 指向运行时常量池的方法引用
- 每一个栈帧的内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在 class 文件的常量池中。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
举例说明:比如在 A 方法中调用了 B 方法,这时在 A 的栈帧中会存放 B 方法的在常量池中的引用,当调用 B 方法的时候,A 方法就知道从哪里去调用 B 方法了。这就是所谓的动态链接。(在我看来动态链接也是一个存储空间,里面存放着该方法需要调用的各种数据的引用)
如图所示:动态链接中存放的就是方法的引用地址,方便当前方法调用需要的方法。
6、方法返回地址
存放该方法的 PC 寄存器的值 (本质也是存放地址的空间)
例如 A 方法中调用 B 方法,那么 B 方法中的方法返回地址就存放着 A 方法调用 B 方法字节码指令的下一个指令地址。当 B 方法执行完正常退出时,就会把方法返回地址中存的地址返回给执行引擎,并更新 PC 寄存器中的地址,这样程序就可以在 A 方法中继续向下执行了。
7、一些附加信息
栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息。可有可没有