专家视角看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 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
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);
}

第四阶段:跃迁跳板 (JavaCallscall_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 方法所需的栈空间。
    • 它会执行 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 能够实现跨语言的异常传递和混合栈回溯。

相关推荐
a9511416422 小时前
CSS 悬停箭头闪烁偏移问题的根源与稳定解决方案
jvm·数据库·python
zhangjw342 小时前
第3篇:Java流程控制:if-else、switch、循环(for/while/do-while)全解析
java·开发语言
Shorasul2 小时前
安装宝塔面板提示端口被占用_查找并终止占用进程
jvm·数据库·python
REDcker2 小时前
C++ std::move实现原理与vector扩容移动语义
开发语言·c++·c
2401_871696522 小时前
macOS 中使用 launchd 每分钟执行一次 PHP 脚本的完整配置指南
jvm·数据库·python
脱氧核糖核酸__2 小时前
LeetCode热题100——48.旋转图像(题解+答案+要点)
c++·算法·leetcode
吕源林2 小时前
MongoDB副本集在网络闪断后如何快速恢复_重连机制与心跳超时(electionTimeoutMillis)
jvm·数据库·python
四斤年华2 小时前
关于SpringBoot在MultipartFile上java.nio.file.NoSuchFileException: /tmp/undertow
java·spring boot·nio
木井巳2 小时前
【递归算法】字母大小写全排列
java·算法·leetcode·决策树·深度优先