前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。


Java的线程是如何run起来的过程

在 OpenJDK 8中,从 C++ 运行环境切换到 Java 字节码执行环境是一次极其精密的“跨次元”跳跃。当底层操作系统完成了 pthread_create 的系统调用后,新线程便在 java_start 睁开了眼睛。

以下是这一过程的深度源码追踪与逻辑还原。


第一阶段:OS 层的“觉醒” (java_start)

当 Linux 内核调度器首次将时间片分配给这个新创建的轻量级进程(LWP)时,它开始执行在 os::create_thread 中指定的启动函数。

  • 源码位置src/os/linux/vm/os_linux.cpp
  • 职责:这是新线程在用户态执行的第一行 C++ 代码。它必须先完成“身份认证”。
  • 关键动作
    1. 设置 TLS (Thread Local Storage):通过 TLS(线程本地存储)将当前的 JavaThread* 存储在 CPU 寄存器(x86-64 下通常是 R15)中,确保线程能随时找回自己的 JVM 句柄。
    2. 同步握手:此时父线程可能还在 JVM_StartThread 中等待,新线程会通过信号量通知父线程:“我已经成功出生,可以继续了”。
    3. 跃入 JVM 核心:调用 thread->run()

第二阶段:JVM 核心整备 (JavaThread::run)

此时,线程虽然还在 C++ 环境中,但已经开始按照 JVM 的规矩办事。

  • 源码位置src/share/vm/runtime/thread.cpp
  • 职责:配置线程的“生存环境”。
  • 关键动作
    1. 栈警戒页 (Stack Guard Pages):调用 os::create_stack_guard_pages(),通过 mprotect 系统调用在物理栈底划出 Yellow PageRed Page。这是 StackOverflowError 能够被安全捕获的底层硬件基础。
    2. 状态切换:将线程状态从 _thread_new 切换为 _thread_in_vm
    3. 调用 thread_main_inner():进入最终的调度中枢。

第三阶段:寻找 Java 入口 (thread_main_inner)

thread_main_inner 中,JVM 需要找到那个在 Java 层定义的 public void run()

  • 逻辑流转
    1. JVM 访问 Java 层的 Thread 对象(通过 eetop 找到的关联对象)。
    2. 获取 run() 方法的 Method* 指针。
    3. 核心调用:执行 JavaCalls::call_virtual
// thread_entry 的典型逻辑
static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
  Handle obj(THREAD, thread->threadObj());
  JavaValue result(T_VOID);
  // 关键动作:准备通过 JavaCalls 调用 Java 层的 run 方法
  JavaCalls::call_virtual(&result, obj, 
                         KlassHandle(THREAD, SystemDictionary::Thread_klass()),
                         vmSymbols::run_method_name(),
                         vmSymbols::void_method_signature(),
                         CHECK);
}

第四阶段:跃迁跳板 (JavaCallscall_stub)

这是整条链路中最硬核的部分。JavaCalls 负责将 C++ 的参数封装成 Java 栈帧能理解的布局。

  • 源码位置src/share/vm/runtime/javaCalls.cpp
  • 跳板指令
    // 最终会调用到这里
    os::os_exception_wrapper(address_of_stub, ...);
    
  • call_stub 的真身:它不是一段 C++ 代码,而是由 StubGenerator 在 JVM 启动时动态生成的纯汇编机器码(位于 src/cpu/x86/vm/stubGenerator_x86_64.cpp 中的 generate_call_stub)。

第五阶段:汇编级跃迁——call_stub 内部发生了什么?

当 CPU 执行到 call_stub 所在的内存区域时,它完成了一场物理层面的环境重塑:

  1. 保存 C++ 现场:将当前 C++ 环境的寄存器(如 RBP, RBX, R12-R15)压入系统栈。
  2. 重置栈指针 (RSP)
    • call_stub 会计算 Java 方法所需的栈空间。
    • 它会执行 movsub 指令,移动 RSP 到一个新的位置,这个位置之上是 C++ 栈,之下则是崭新的 Java 栈
  3. 参数对齐:将 Java 方法需要的参数(如 this 指针,即 Thread 对象)从 C++ 的寄存器或栈位置拷贝到 Java 寄存器调用约定(如 RSI, RDX 等)或 Java 栈帧中。
  4. 设置 Anchor (锚点):在 JavaFrameAnchor 中标记当前的栈顶位置,这是为了后续 GC 能够准确回溯栈帧。
  5. 终极跳转 (call / jmp)
    • call_stub 获取 Thread.run() 方法的 Entry Point(入口地址)。
    • 如果是初次执行,这通常指向解释器入口 (Interpreter Entry)
    • 瞬间跳跃:随着一条 call 指令,PC 寄存器指向了字节码解释器的首条指令。

总结:从死到生的瞬间

在 OpenJDK 8u44 的语境下,这个过程可以概括为:

  • java_start 是肉身的出生证明。
  • JavaThread::run 是生存空间的划定。
  • call_stub 是连接两个世界的时空隧道。

Thread.run() 的第一条字节码(通常是 aload_0)被解释器加载到寄存器时,这个线程才真正完成了它的“成人礼”,从操作系统的 LWP 变成了一个活生生的 Java 线程。

这种设计的精妙之处在于,物理栈是连续的,但逻辑栈是断裂的call_stub 正是那个缝合断裂、转换协议的唯一关口。理解了这一点,你也就理解了为什么 JVM 能够实现跨语言的异常传递和混合栈回溯。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐