JVM线程栈内存管理机制剖析
线程栈内存管理机制剖析
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
线程栈内存管理机制剖析
在 Linux x86_64 架构下,HotSpot JVM 的线程栈采用的是向下生长(从高地址向低地址)的内存模型。JVM 对线程栈的控制并非完全交由操作系统,而是通过精确计算和内存保护页(Guard Pages)实现了用户态与内核态协同的精细化管理。
在 HotSpot VM 中,Java 线程(JavaThread)与操作系统的原生线程(在 Linux 上为 pthread)是一对一映射的。JVM 线程栈的生命周期管理主要涉及内核级栈分配、用户级空间规划(栈下溢/上溢保护)以及信号捕捉与异常恢复。
1. JVM 线程栈内存布局架构
在 x86_64 架构的 Linux 系统中,栈是向低地址延伸的。JVM 为了防止栈溢出破坏其他内存区域,并在溢出时能抛出 Java 级别的 StackOverflowError,将线程栈划分为了不同的功能区域:
- Stack Base(栈底):高地址端,栈的起始位置。
- Java Frames(方法栈帧区):执行 Java 方法和本地方法(Native Method)的区域。
- Shadow Zone(阴影区):为原生方法或 JVM 内部调用预留的缓冲空间(默认由
-XX:StackShadowPages控制),确保在触发常规栈溢出检查前,JNI 代码有足够的栈空间执行。 - Yellow Zone(黄色警戒区):触及此区域表示栈即将耗尽(默认由
-XX:StackYellowPages控制)。JVM 会将其权限设为不可读写,触发SIGSEGV,随后信号处理器将其恢复并抛出StackOverflowError。 - Red Zone(红色死区):绝对不允许触及的区域(默认由
-XX:StackRedPages控制)。一旦触及,说明连异常处理的栈空间都不够了,JVM 将直接触发 Fatal Error 并崩溃。 - Stack Limit(栈顶限界):低地址端,整个栈的终点。
2. OpenJDK 8源码深度解析
以下基于 OpenJDK 8核心源码,追踪线程栈的创建、初始化、物理保护(Guard Pages)的实现流程。
阶段一:OS 线程创建与栈大小确立
当在 Java 中调用 new Thread().start() 时,JVM 内部会创建对应的 JavaThread,并调用平台相关的操作系统接口(以 Linux 为例)来分配栈。
源码文件:src/os/linux/vm/os_linux.cpp
bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {
assert(thread->osthread() == NULL, "sanity check");
// 1. 分配并初始化操作系统层面的 OSReturn/OSThread 结构体
OSThread* osthread = new OSThread(NULL, NULL);
if (osthread == NULL) {
return false;
}
// 设置线程类型(此处为 java_thread)
osthread->set_thread_type(thr_type);
// 2. 计算线程栈大小
// 如果没有显式指定 stack_size,则根据线程类型获取默认值
if (stack_size == 0) {
switch (thr_type) {
case os::java_thread:
// 取 -Xss 或 -XX:ThreadStackSize 的配置值(单位为 KB,需转为字节)
stack_size = JavaThread::stack_size_at_creation();
break;
case os::compiler_thread:
if (CompilerThreadStackSize > 0) {
stack_size = (size_t)CompilerThreadStackSize * K;
}
break;
case os::vm_thread:
case os::pgc_thread:
case os::cgc_thread:
if (VMThreadStackSize > 0) {
stack_size = (size_t)VMThreadStackSize * K;
}
break;
}
}
// 确保栈大小不低于操作系统的最低限制
stack_size = MAX2(stack_size, os::Linux::min_stack_allowed);
// 3. 设置 pthread 属性
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 关键点:将计算好的栈大小传给 pthread 属性
// Linux 内核随后会通过 mmap(..., MAP_PRIVATE | MAP_ANONYMOUS, ...) 分配这块虚拟内存
if (stack_size > 0) {
pthread_attr_setstacksize(&attr, stack_size);
}
// 4. 调用 pthread_create 创建原生线程
pthread_t tid;
int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);
pthread_attr_destroy(&attr);
if (ret != 0) {
// 创建失败,释放资源并返回
delete osthread;
return false;
}
// 成功绑定 OS 线程 ID
osthread->set_pthread_id(tid);
thread->set_osthread(osthread);
return true;
}
阶段二:获取并记录栈的真实边界
操作系统创建好线程后,会回调 java_start 函数。在这个阶段,子线程需要准确获取操作系统实际分配的栈基址(Base)和大小(Size)。
源码文件:src/os/linux/vm/os_linux.cpp
static void *java_start(Thread *thread) {
// 获取当前线程的 OS 句柄
OSThread* osthread = thread->osthread();
monitor_init_completion(osthread);
// 1. 动态捕获当前 pthread 真实的栈基址和大小
// 必须通过 pthread_attr_getstack 获取,因为 OS 可能会对齐页边界,导致实际大小与传入大小有微调
pthread_attr_t attr;
address kstack_base = NULL;
size_t kstack_size = 0;
pthread_getattr_np(pthread_self(), &attr);
pthread_attr_getstack(&attr, (void**)&kstack_base, &kstack_size);
pthread_attr_destroy(&attr);
// 2. 转换并记录栈边界(Linux 栈向下生长,kstack_base 是内存低地址,base = low_addr + size 是高地址)
address stack_base = kstack_base + kstack_size;
thread->set_stack_base(stack_base);
thread->set_stack_size(kstack_size);
// 3. 调用具体的 JavaThread 初始化逻辑(内部会建立内存保护页)
thread->run();
return NULL;
}
阶段三:部署物理警戒页(Guard Pages)
当线程正式开始运行 Java 代码前,必须在栈的低地址端(即栈顶延伸的极限位置)部署 Yellow Zone 和 Red Zone。这是通过操作系统的物理内存保护机制 mprotect 实现的。
源码文件:src/share/vm/runtime/thread.cpp
void JavaThread::create_stack_guard_pages() {
// 如果已经启用了保护页,或者 OS 不支持,则直接返回
if (!os::uses_stack_guard_pages() || _stack_guard_state == stack_guard_enabled) {
return;
}
// 1. 计算警戒区的总大小
// 总页数 = StackYellowPages + StackRedPages(默认通常是 3页 + 1页)
size_t size = stack_guard_zone_size();
// 2. 定位警戒区在内存中的低地址起始点
// 栈底(高地址)减去总大小,得到保护页的基地址
address low_addr = stack_base() - stack_size();
// 3. 跨平台调用:对该段内存区域实施底层的物理保护
// 核心是将该区域的权限修改为 PROT_NONE(不可读、不可写、不可执行)
if (!os::guard_memory((char*)low_addr, size)) {
warning("An error occurred trying to set memory protections. Your system may be out of memory.");
return;
}
// 标记保护页状态为已启用
_stack_guard_state = stack_guard_enabled;
}
源码文件:src/os/linux/vm/os_linux.cpp (底层系统调用映射)
bool os::guard_memory(char* addr, size_t bytes) {
// linux_mprotect 封装了 Linux 的 mprotect 系统调用
// PROT_NONE 意味着任何对该内存页的访问(读/写/执行)都会直接引发 CPU 硬件级别的页错误(Page Fault)
// 进而被 Linux 内核转化为 SIGSEGV 信号发送给 JVM 进程
return linux_mprotect(addr, bytes, PROT_NONE);
}
3. 栈溢出(StackOverflowError)的底层触发与拦截机制
当 Java 方法调用层级过深,线程栈的 rsp(栈指针寄存器)向下推进,最终必然会触及到被设置为 PROT_NONE 的警戒区。以下是整个硬件到软件的连锁反应机制:
[硬件/内核层]
RSP 指针写入物理保护页 (PROT_NONE)
--> 触发 CPU Page Fault
--> Linux 内核发送 SIGSEGV 信号给 JVM
[JVM 层: 信号处理器]
JVM_handle_linux_signal() 拦截 SIGSEGV
--> 检查 fault_address 是否在当前线程的 Yellow/Red Zone
--> 匹配成功:执行异常恢复流程
信号处理核心源码解析
源码文件:src/os_cpu/linux_x86/vm/os_linux_x86.cpp
JVM_handle_linux_signal(int sig, siginfo_t* info, void* uc, int abort_if_unrecognized) {
// 仅处理不涉及内核崩溃的标准信号,如 SIGSEGV
if (sig == SIGSEGV || sig == SIGBUS) {
address addr = (address) info->si_addr; // 获取引发缺页异常的物理内存地址
// 获取当前正在运行的 Java 线程指针
Thread* t = ThreadLocalStorage::get_thread_by_slot();
if (t != NULL && t->is_Java_thread()) {
JavaThread* thread = (JavaThread*)t;
// 检查异常地址是否落在当前线程的栈空间范围内,且位于警戒区内
if (thread->in_stack_yellow_zone(addr)) {
// 【命中黄色警戒区】:
// 1. 立即解除黄色警戒区的内存保护(恢复为可读写),以便有足够的栈空间来执行 Java 异常处理代码
thread->disable_stack_yellow_zone();
// 2. 调整程序计数器(PC),准备抛出 Java 级别的 StackOverflowError
if (thread->thread_state() == _thread_in_Java) {
// 修正 stub 代码,跳转到专门构建的物理异常抛出点
stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::STACK_OVERFLOW);
}
// 更新由信号上下文(ucontext_t)决定的返回地址,完成劫持
((ucontext_t*)uc)->uc_mcontext.gregs[REG_RIP] = (greg_t)stub;
return true;
} else if (thread->in_stack_red_zone(addr)) {
// 【命中红色死区】:
// 连黄色警戒区都没防住(例如 Native 代码狂暴消耗栈),触及最后的底线
// 1. 释放红区保护,防止后续二次崩溃
thread->disable_stack_red_zone();
// 2. 无法恢复,直接打印 Native 级的 Terminate 错误日志,触发 VM 致命退出
tty->print_cr("An irrecoverable stack overflow has occurred.");
report_fatal_error_in_vm("Stack overflow error in Red Zone", __FILE__, __LINE__);
}
}
}
// 如果不是 JVM 引起的,交由系统原生的信号管道处理
return false;
}
当 Yellow Zone 被解除保护并成功抛出 StackOverflowError 后,Java 的 catch 块会捕获到该异常。在 JVM 完成异常栈帧的弹栈(Unwind)并重回安全区域后,会在合适的时间点重新调用 JavaThread::reguard_stack_guard_pages(),重新将黄色警戒区设置为 PROT_NONE,以此循环保护整个线程栈的安全。
4. 线程栈内存生命周期总结
[Java 线程启动]
│
▼
[os::create_thread] ────► 计算物理页对齐的栈大小 ────► 调用 glibc pthread_create
│
▼
内核开辟匿名虚拟内存 (VMA)
│
┌─────────────────────────────────────────────────────┘
▼
[JavaThread::run] ───► 调用 os::guard_memory ───► 将栈底的 Yellow/Red 页通过 mprotect 设为 PROT_NONE
│
▼
[Java 方法深度调用] ─► 栈指针 (RSP) 触碰 Yellow Zone ───► 触发 CPU 硬件缺页中断
│
▼
内核投递 SIGSEGV 信号
│
┌─────────────────────────────────────────────────────┘
▼
[JVM_handle_linux_signal]
│
├─► [地址在 Yellow Zone] ──► 动态恢复权限 ──► 注入异常跳转 ──► 抛出 StackOverflowError
│
└─► [地址在 Red Zone] ──► 放弃抵抗 ──► 终止 JVM 进程 ──► 生成 hs_err_pid.log
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)