前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
Java 线程栈的“真相”
在 OpenJDK的实现中,Java 线程栈的“真相”可以用一句话概括:所谓的 Java 线程栈,物理上就是操作系统分配的 Native Stack,逻辑上被 JVM 划分成了不同的区域。
下面我们通过源码的执行路径,看看 JVM 是如何从操作系统“骗”来这块内存并将其改造为 Java 栈的。
1. 内存申请:os::create_thread阶段
当你调用Thread.start(),最终会进入平台相关的线程创建函数。在 Linux 上,这就是src/os/linux/vm/os_linux.cpp。
核心源码片段 1:确定栈大小
JVM 首先要计算这个系统线程需要多大的栈。
// src/os/linux/vm/os_linux.cppboolos::create_thread(Thread*thread,ThreadType thr_type,size_t stack_size){...// 如果没有指定 -Xss,则根据线程类型获取默认栈大小if(stack_size<=0){stack_size=JavaThread::stack_size_at_create();}// 调整栈大小,确保包含 Guard Pages (警戒页)stack_size=MAX2(stack_size,os::Linux::min_stack_allowed);...pthread_attr_t attr;pthread_attr_init(&attr);// 关键:将 JVM 参数传递给 pthread 库pthread_attr_setstacksize(&attr,stack_size);真相:此时,内存还没有真正分配。pthread_attr_setstacksize只是告诉内核:“待会儿给我创建线程时,请预留这么大的虚拟地址空间”。
2. 内存绑定:JavaThread对象的初始化
当pthread_create成功后,子线程开始运行thread_native_entry。这时,JVM 需要把这块系统内存“锚定”到JavaThread对象上。
核心源码片段 2:记录栈边界
// src/share/vm/runtime/thread.cppvoidJavaThread::record_stack_base_and_size(){// 获取当前系统线程的栈底和大小address low_addr;size_t size;if(os::current_stack_size(&size,&low_addr)){// _stack_base 是高地址(栈生长的起点)_stack_base=low_addr+size;_stack_size=size;}}真相:JavaThread并不拥有这块内存,它只是通过_stack_base和_stack_size记录了这块系统分配内存的坐标。后续所有的 Java 栈帧压栈,本质上都在这块坐标范围内移动RSP寄存器。
3. 栈的逻辑分区:Guard Pages 的创建
为了防止 Java 代码递归太深导致系统崩溃,JVM 会在申请到的系统栈底部手动挖掘“陷阱”。
核心源码片段 3:设置警戒页
// src/share/vm/runtime/thread.cppvoidJavaThread::create_stack_guard_pages(){// 计算黄色警戒区和红色警戒区的位置// 通过 mprotect 系统调用,将对应的系统内存页设为“不可访问”if(os::guard_memory((address)low_addr,guard_size)){_stack_guard_state=stack_guard_enabled;}}真相:这是 Java 栈和普通系统栈最大的不同。JVM 通过mprotect将栈底的几 KB 物理内存标记为禁止访问。
- Yellow Zone:如果 CPU 访问到这里,说明栈快满了,JVM 捕获信号并抛出
StackOverflowError。 - Red Zone:如果访问到这里,说明已经彻底失控,JVM 会直接打印
Native Crash并强行退出。
4. 运行时真相:call_stub的压栈操作
当我们在本系列之前聊到的call_stub介入时,它如何处理这个栈?
核心源码片段 4:汇编层面的栈切换
在src/cpu/x86/vm/stubGenerator_x86_64.cpp中:
__movptr(r13,entry_point);// 获取 Java 方法入口__mov(rbp,rsp);// 建立 C++ 栈帧基址// 接下来是关键:在当前系统栈上开辟 Java 区域__subptr(rsp,locals_size);// 移动 RSP,为 Java 局部变量腾出空间真相:call_stub并没有切换到另一块内存,它只是在同一个pthread栈上,通过减小RSP(栈指针)的值,划出一块地盘给 Java 使用。
5. 总结:关于栈空间的三个终极真相
| 维度 | 真相描述 |
|---|---|
| 分配本质 | Java 栈就是Native 内存。由mmap分配,不受 JVM GC 管理。 |
| 内存成本 | 初始分配的是虚拟内存。只有当方法调用变深、触碰新页时,才会消耗物理内存 (RSS)。 |
| 安全机制 | 通过操作系统的页保护(Memory Protection)来实现StackOverflowError,而不是靠逻辑判断。 |