Skip to content

虚拟机栈特点/异常/栈大小设置

虚拟机栈:方法调用的「账本」

每当你调用一个方法,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 默认
Windows320KB(Java 6 之后)取决于系统配置
Linux1MB取决于系统配置
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,根据线程数和递归深度需求调整。

下一节,我们来看 栈存储结构与栈帧内部结构,深入理解栈帧的构成。

基于 VitePress 构建