Appearance
虚拟机栈
虚拟机栈是线程私有区域。每个 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是否被设置得过大或过小。- 容器内存是否给线程栈留了预算。
