虚拟机栈特点/异常/栈大小设置
虚拟机栈:方法调用的「账本」
每当你调用一个方法,JVM 就在当前线程的虚拟机栈中分配一块空间,叫栈帧(Stack Frame)。方法执行完毕,栈帧弹出。方法里的局部变量、操作数、返回值,都存在这块空间里。
虚拟机栈的基本特性
线程私有
虚拟机栈是线程私有的。每个线程有自己的栈,线程之间完全隔离,互不干扰。
java
public class StackPrivate {
public static void main(String[] args) {
// main 线程的虚拟机栈
// 包含 main() 方法的栈帧
new Thread(() -> {
// 新线程有自己的虚拟机栈
// 独立于 main 线程的栈
doSomething();
}).start();
}
static void doSomething() {
// 新线程的栈中会有 doSomething() 的栈帧
// 里面有自己的局部变量
}
}生命周期与线程同步
虚拟机栈的生命周期与线程完全同步:线程启动时分配,线程结束时回收。不需要 GC——线程结束,栈自动释放。
后进先出(LIFO)
栈的核心特性是后进先出。方法调用的嵌套,对应栈帧的压入和弹出:
java
public class StackLIFO {
public static void main(String[] args) {
methodA(); // 栈帧 A 入栈
}
static void methodA() {
int a = 1;
methodB(); // 栈帧 B 入栈
// methodB 执行完后,B 弹出,A 继续
}
static void methodB() {
int b = 2;
methodC(); // 栈帧 C 入栈
// methodC 执行完后,C 弹出,B 继续
}
static void methodC() {
int c = 3;
// 最先执行,最后返回
// methodC 返回后,C 弹出,B 继续
// B 返回后,B 弹出,A 继续
// A 返回后,A 弹出,栈空
}
}调用顺序:main → A → B → C
返回顺序:C → B → A → main
栈变化: 入→入→入→出→出→出栈内存可以抛出两类异常
JVM 规范定义了虚拟机栈可能抛出的两种异常:
1. StackOverflowError:栈溢出
当线程请求的栈深度超过了 JVM 允许的最大深度,会抛出 StackOverflowError。
最常见的场景:递归调用没有终止条件
java
public class StackOverflowDemo {
public static void main(String[] args) {
recursive(); // 无限递归
}
static void recursive() {
recursive(); // 栈帧无限压入,直到溢出
}
}Exception in thread "main" java.lang.StackOverflowError
at StackOverflowDemo.recursive(StackOverflowDemo.java:5)
at StackOverflowDemo.recursive(StackOverflowDemo.java:5)
at StackOverflowDemo.recursive(StackOverflowDemo.java:5)
...(无限重复)2. OutOfMemoryError:内存溢出
当 JVM 尝试扩展栈时发现内存不足(通常是因为创建了太多线程),会抛出 OutOfMemoryError。
java
public class OOMByManyThreads {
public static void main(String[] args) {
// 不断创建线程
while (true) {
new Thread(() -> {
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {}
}).start();
}
}
}栈大小设置
默认大小
JVM 栈的默认大小取决于操作系统和 JVM 版本:
| 系统 | 32 位 JVM 默认 | 64 位 JVM 默认 |
|---|---|---|
| Windows | 320KB(Java 6 之后) | 取决于系统配置 |
| Linux | 1MB | 取决于系统配置 |
| macOS | 取决于 JVM 版本 | 取决于 JVM 配置 |
JDK 8+ 的 HotSpot 通常把默认栈大小设为 1MB(-Xss1024k)。
相关参数
bash
# 设置栈大小为 512KB
java -Xss512k MyApp
# 设置栈大小为 2MB
java -Xss2m MyApp
# 打印默认栈大小
java -XX:+PrintFlagsFinal -version | grep ThreadStackSize合理设置的原则
-Xss 设置值 太大 ──→ 线程数减少(总内存固定)
-Xss 设置值 太小 ──→ 递归深度受限,容易 StackOverflowError| 场景 | 推荐设置 |
|---|---|
| 递归层级深 | 增大 -Xss(如 2m),减少 StackOverflowError |
| 需要大量线程 | 减小 -Xss(如 256k),增加可创建的线程数 |
| 内存受限环境 | 减小 -Xss,腾出更多堆空间 |
| 标准企业应用 | 使用默认值(1MB),通常不需要调整 |
计算线程数上限
一个简单公式帮助估算能创建多少线程:
最大线程数 ≈ (可用物理内存 - 堆内存 - 元空间 - 其他开销) / 栈大小例如:4GB 内存,堆设 2GB,元空间 256MB,应用占用 512MB,剩余约 1.2GB:
线程数上限 ≈ 1.2GB / 1MB ≈ 1200 个线程(理论值,实际会更少)HotSpot 虚拟机的栈实现
HotSpot 的虚拟机栈实际上由两部分组成:
虚拟机栈(HotSpot 实现)
├── Java 虚拟机栈(Java 方法服务)
└── 本地方法栈(native 方法服务)两者合称「C栈」,但 JVM 规范将它们分开定义。HotSpot 将两者合一实现:-Xoss 参数实际上无效(历史遗留),只有 -Xss 同时控制两者。
本节小结
虚拟机栈的关键特性:
| 特性 | 说明 |
|---|---|
| 线程私有 | 每个线程独立栈 |
| 后进先出 | 方法调用对应栈帧的压入和弹出 |
| 不需要 GC | 线程结束自动释放 |
| 固定或动态 | 可以是固定大小,也可以动态扩展(依赖实现) |
两类异常:
| 异常 | 原因 | 典型场景 |
|---|---|---|
StackOverflowError | 栈深度超限 | 无限递归 |
OutOfMemoryError | 无法扩展栈(内存不足) | 线程数过多 |
栈大小设置:java -Xss<size>,默认值约 1MB,根据线程数和递归深度需求调整。
下一节,我们来看 栈存储结构与栈帧内部结构,深入理解栈帧的构成。
