Skip to content

虚拟机栈

虚拟机栈是线程私有区域。每个 Java 方法调用都会创建一个栈帧,方法返回时栈帧出栈。它解释了方法调用、局部变量、递归、异常栈和 StackOverflowError

栈帧里有什么

组成作用
局部变量表保存参数、局部变量、对象引用
操作数栈字节码执行时的临时计算区
动态链接指向运行时常量池中的方法引用
返回地址方法执行完回到调用方的位置

一个线程一条栈,一个方法调用一个栈帧:

text
Thread
  -> frame: Controller.method()
  -> frame: Service.method()
  -> frame: Repository.method()

常见问题

StackOverflowError

常见原因:

  • 递归没有终止条件。
  • 对象 toString()equals() 互相递归。
  • 框架代理或拦截器形成循环调用。
  • 单次调用链过深。

排查:

bash
jstack <pid> > thread.txt

看异常栈中是否出现大量重复方法。如果重复模式明显,优先找递归出口或循环调用。

线程太多导致内存压力

每个线程都有自己的栈。-Xss 设置越大,同样内存下可创建线程数越少。

bash
-Xss1m

如果应用创建大量线程,进程 RSS 会被线程栈推高,即使堆并不高。

排查:

bash
jcmd <pid> Thread.print
ps -M <pid>

线程池应设置合理上限,不要无限创建线程。

参数取舍

调整影响
增大 -Xss单线程可承受更深调用,但总线程容量下降
减小 -Xss支持更多线程,但深调用更容易栈溢出

不要把 StackOverflowError 直接用增大 -Xss 掩盖。多数情况下应先修正递归或循环调用。

与堆的关系

栈帧中的局部变量可能保存对象引用,真正对象通常在堆中。只要栈上变量仍然引用对象,该对象就是可达的,GC 不能回收。

常见误解:

  • 局部变量本身在栈里,但对象不一定在栈里。
  • 方法返回后,栈帧释放,对局部对象的引用消失,对象才可能被 GC。
  • 栈溢出不是堆不够,而是线程调用深度或栈大小问题。

排查清单

  • 异常栈是否有重复方法。
  • 是否有递归、代理链、事件回调互相调用。
  • 线程数是否异常增长。
  • -Xss 是否被设置得过大或过小。
  • 容器内存是否给线程栈留了预算。
别急,先让缓存热一下。