虚拟机栈本地方法栈与程序计数器
除了堆和方法区,JVM 中还有几个线程私有的运行时区域:虚拟机栈、本地方法栈和程序计数器。它们主要服务于线程执行和方法调用。
# 1. 线程私有是什么意思
线程私有表示每个线程都有自己独立的一份内存区域。
线程A -> 自己的程序计数器、虚拟机栈、本地方法栈
线程B -> 自己的程序计数器、虚拟机栈、本地方法栈
线程私有区域通常不需要 GC 管理,因为它们随线程创建和销毁。
# 2. 虚拟机栈
虚拟机栈保存 Java 方法调用过程。
每次方法调用创建一个栈帧:
methodA -> methodB -> methodC
对应栈结构:
methodA栈帧
methodB栈帧
methodC栈帧
当前执行的是栈顶方法。
# 3. 栈大小参数
线程栈大小可以通过参数设置:
-Xss1m
栈越大:
- 单个线程可承载更深调用。
- 同样内存下可创建线程数减少。
栈越小:
- 可创建更多线程。
- 更容易出现
StackOverflowError。
# 4. 本地方法栈
本地方法栈用于执行 native 方法。
Java 标准库和 JVM 内部很多底层能力会涉及 native 方法,例如:
- 文件系统。
- Socket。
- 压缩。
- 加密。
- 系统时间。
本地方法栈异常也可能表现为 StackOverflowError 或 OutOfMemoryError。
# 5. 程序计数器
程序计数器保存当前线程执行到哪条字节码指令。
多线程切换时,CPU 会在线程之间切换执行。程序计数器帮助线程恢复后继续从正确位置执行。
它的特点:
- 线程私有。
- 占用空间小。
- 不会发生 OOM。
- native 方法时值通常未定义。
# 6. 无法创建新线程
大量线程可能导致:
java.lang.OutOfMemoryError: unable to create native thread
这不一定是 Java 堆不够,而可能是:
- 操作系统线程数限制。
- 进程可用内存不足。
- 单线程栈过大。
- 容器资源限制。
排查时要看线程数、-Xss、系统限制、容器内存。
# 7. 阶段小结
虚拟机栈负责 Java 方法调用,本地方法栈负责 native 方法调用,程序计数器负责记录线程执行位置。它们是线程私有区域,常见问题包括栈溢出和无法创建新线程。
# 8. 线程私有区域结构图
Thread-1
├─ PC Register
├─ Java Virtual Machine Stack
│ ├─ frame: controller()
│ ├─ frame: service()
│ └─ frame: repository()
└─ Native Method Stack
Thread-2
├─ PC Register
├─ Java Virtual Machine Stack
└─ Native Method Stack
每个线程都有独立的执行位置和调用栈,因此一个线程抛出栈溢出,不代表其他线程栈也有问题。
# 9. -Xss 和线程数量估算
假设进程可用于线程栈的本地内存约为 512MB:
-Xss | 理论可支持线程数上限 |
|---|---|
| 1MB | 约 512 个 |
| 512KB | 约 1024 个 |
| 256KB | 约 2048 个 |
这是非常粗略的估算,实际还受操作系统、容器、JVM 内部内存、线程库开销影响。
工程上更重要的是:不要靠无限创建线程解决并发问题,应该控制线程池。
# 10. 常见线程栈问题
# 10.1 深递归
void dfs(Node node) {
for (Node child : node.children()) {
dfs(child);
}
}
如果树深度不可控,可能栈溢出。可以改成显式栈:
Deque<Node> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
Node node = stack.pop();
// process
}
# 10.2 线程池失控
如果每个请求都创建线程,或者线程池最大线程数过高,可能导致 native thread OOM。
# 10.3 栈上引用影响 GC
栈帧中的局部变量引用是 GC Roots。一个长时间运行的方法如果持有大对象引用,即使后面逻辑不再使用,也可能延长对象生命周期。
# 11. Tips 快问快答
Q:StackOverflowError 能不能靠加大 -Xss 解决?
A:只能缓解,不能解决无限递归或不合理递归深度。根因通常要改代码。
Q:为什么线程太多会导致 OOM,而不是 CPU 高?
A:线程需要本地栈内存和系统线程资源,数量过多会先耗尽内存或线程配额。
Q:程序计数器会发生 OOM 吗?
A:JVM 规范中程序计数器是唯一没有规定 OOM 的运行时区域。
Q:RUNNABLE 线程一定在占 CPU 吗?
A:不一定。它可能在运行,也可能在 native IO 等待。要结合系统线程 CPU 使用判断。