虚拟机栈(线程私有)
由于跨平台性的设计,Java 的指令都是根据栈来设计的。不同平台 CPU 架构不同,所以不能设计为基于寄存器的。
一、基本概念
1、概述
栈 vs 堆:
栈
:解决的是程序运行
的问题,即程序如何执行,或者说如何处理数据。堆
:解决的是数据存储
的问题,即数据怎么放,放哪里
虚拟机栈的生命周期:
- 和线程的生命周期一致
虚拟机栈的作用:
- 描述 Java方法 的内存模型,保存方法的局部变量、部分结果,并参与方法的调用和返回。
栈的特点:
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
- 对于栈来说不存在垃圾回收问题(但是栈存在SOF和OOM的问题)
- 栈的大小直接决定了方法调用的最大可达深度
2、设置栈内存大小
# 栈
-Xss128k 设置每个线程的栈大小(包括初始大小以及栈的动态扩展行为)
-XX:ThreadStackSize=128k 设置每个线程的初始栈大小(只影响每个线程的初始栈大小)
3、栈相关的异常
在不同的 Java 虚拟机实现中,对于栈的大小可以有不同的处理方式,可以是 动态扩展的 或 固定不变的:
-
固定不变的 Java 虚拟机栈
在虚拟机启动时就确定了。在这种情况下,虚拟机会为每个线程分配固定大小的栈空间,并且不会随着程序的运行而动态调整。这样的实现可能会更简单、更高效,但是如果栈空间不足,就有可能导致栈溢出异常(
StackOverflowError
)。 -
动态扩展的 Java 虚拟机栈
虚拟机栈的大小可以根据程序的需求动态增长。这种情况下,虚拟机会根据需要动态地调整栈的大小,以满足程序运行时的需求。这样可以在一定程度上避免因为栈空间不足而导致的栈溢出问题。不过,动态扩展也可能带来一些性能开销。
对于 HotSpot VM,一般来说,虚拟机栈的大小是固定的,这个固定的大小由 -Xss
参数来设置。
1)StackOverflowError
如果 线程请求分配的栈容量 超过 虚拟机栈允许的最大容量,就会抛出StackOverflowError
异常。(常见于递归)
package stack;
/**
* 栈超出最大深度:StackOverflowError
* - 默认情况下:count:31857
* - VM options 设置栈的大小:-Xss512k count:4924
*/
public class StackSOF {
private int stackLength = 1;
// 递归
public void recursion() {
stackLength++;
recursion();
}
public static void main(String[] args) {
StackSOF stackSOF = new StackSOF();
try {
stackSOF.recursion();
} catch (Throwable e) {
System.out.println("当前栈深度:" + stackSOF.stackLength);
e.printStackTrace();
}
}
}
2)OutOfMemoryError
-
在尝试扩展的时候无法申请到足够的内存,就会抛出
OutOfMemoryError
异常。 -
在创建新线程的时候没有足够的内存去创建对应的虚拟机栈,也会抛出
OutOfMemoryError
异常。
以下代码示例谨慎使用,可能会引起电脑卡死
package stack;
/**
* 栈内存溢出: OOM
* VM options 设置栈的大小:-Xss2m
**/
public class StackOOM {
public static void main(String[] args) {
StackOOM stackOOM = new StackOOM();
stackOOM.stackLeakByThread();
}
// 不断创建线程 -> 不断创建Java虚拟机栈 -> 不断申请内存 -> 内存溢出
public void stackLeakByThread() {
while (true) {
Thread t = new Thread(new Runnable() {
public void run() {
dontStop();
}
});
t.start();
}
}
private void dontStop() {
while (true) {
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:719)
at stack.StackOOM.stackLeakByThread(StackOOM.java:22)
at stack.StackOOM.main(StackOOM.java:11)
二、栈的运行原理
每个线程在创建时,都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用
1、栈的存储单位 - 栈帧
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
2、当前栈帧
一个线程,一个时间点上,只会有一个活动的栈帧,这个栈帧被称为 当前栈帧
(Current Frame)
- 与
当前栈帧
相对应的方法就是当前方法
(Current Method) - 定义这个方法的类就是
当前类
(Current Class)
执行引擎 运行的所有字节码指令 只针对 当前栈帧 进行操作。
3、压栈 & 出栈
JVM 直接对 Java 栈的操作只有两个,就是对 栈帧的压栈和出栈,遵循「先进后出」「后进先出」的 原则。
- 压栈:
- 每当方法被调用时,对应的栈帧会被创建出来,压入栈顶。(栈顶的栈帧 即
当前栈帧
) - 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,压入栈顶,成为
新的当前栈帧
。
- 每当方法被调用时,对应的栈帧会被创建出来,压入栈顶。(栈顶的栈帧 即
- 出栈:
- 正常的方法返回(使用 return 指令) 和 抛出异常,都会导致栈帧被弹出。
- 方法返回时,当前栈帧将当前方法的执行结果传给前一个栈帧,接着,当前栈帧出栈,前一个栈帧成为当前栈帧。
注意:不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
4、方法的执行过程
- 方法调用前:创建栈帧
- 方法执行时:栈帧入栈
- 方法执行后:栈帧出栈
三、栈帧的内部结构
栈帧是用来存储数据和部分过程结果的数据结构,每个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或表达式栈)
- 动态链接(DynamicLinking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
- 一些附加信息
有些地方将 动态链接、方法返回地址、附加信息 统称为 帧数据区。
每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要是由 局部变量表 和 操作数栈 决定的。
1、局部变量表(Local Variables)
局部变量表
用于存储方法中的局部变量,包括方法参数
以及在方法中定义的局部变量
。
- 基本数据类型(byte、short、int、long、float、double、char、boolean)
- 对象引用(对象实例存在堆中)
- returnAddress类型(指向了一条字节码地址)
1)特点
- 局部变量表存储在栈帧中,是
线程私有
的,因此 不存在数据安全问题。 - 局部变量表的大小是在编译时确定的,在方法运行期间不会改变。
- 局部变量表中的变量只在
当前方法
的调用中有效。方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
2)Slot 变量槽
局部变量表是一个数组结构,每个元素都存储一个局部变量的值。最基本的存储单元是 Slot(变量槽)
-
32 位以内的类型只占用一个 Slot(包括 returnAddress 类型),64 位的类型占用两个 Slot。
-
引用数据类型(32位),占用一个 Slot
-
int类型(32位),占用一个 Slot
byte、short、char 在存储前被转换为 int,boolean 也被转换为 int,0 表示 false,非 0 表示 true。
-
long类型 和 double类型(64位),占用两个 Slot
-
-
JVM 会为局部变量表中的每一个 Slot 都分配一个
访问索引
,通过这个索引即可成功访问到指定的局部变量值。 -
当方法被调用时,
方法参数
和方法中定义的局部变量
会 按照顺序 被复制到局部变量表中的每一个 Slot 上。
如果是
构造方法
或实例方法
,Index=0 的 Slot 存放的是对象引用this
,其余的参数 按