前言

本文旨在记录近期研读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 ZoneRed 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

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐