专家视角看Java的线程是如何run起来的过程

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

前言

本文旨在记录近期研读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 Page 和 Red 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。
cpp 复制代码
// 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);
}

第四阶段:跃迁跳板 (JavaCalls 与 call_stub)

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

  • 源码位置 :src/share/vm/runtime/javaCalls.cpp

  • 跳板指令 :

    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 方法所需的栈空间。
    • 它会执行 mov 和 sub 指令,移动 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 能够实现跨语言的异常传递和混合栈回溯。

相关推荐
Wang's Blog11 小时前
Java 项目实战: 交友项目 - FastDFS:入门案例
java·开发语言·交友
apcipot_rain12 小时前
C++转Java日记(03)
java·开发语言·c++
皮皮林55112 小时前
Java实现微信扫码登录
java
布莱克60512 小时前
C++ 中 map 的 insert 与 emplace:key 已存在时会覆盖吗?
开发语言·数据结构·c++·算法·map
xys_67813 小时前
深挖C++:类和对象(中)
开发语言·c++
LongRunning14 小时前
【Linux】RK3568-OpenCV+YOLO(八)
linux·opencv·yolo
Yyyyyy~15 小时前
【Java】Day4Web基础知识
java·spring boot
D e re k ZH15 小时前
C++ 系统学习手记:从语法细节到工程实践
开发语言·数据结构·c++·算法
灿飞屋16 小时前
Kotlin与Java虚拟机三:控制流
android·java·开发语言·后端·python·kotlin·安卓
xiaoye-duck16 小时前
《C++ 项目:手写大模型接入 SDK》Ollama接口设计(上):初始化、全量响应实现和测试
开发语言·网络·c++·语言模型