Java的线程是如何run起来的过程
-
- 前言
- Java的线程是如何run起来的过程
-
- [第一阶段:OS 层的"觉醒" (`java_start`)](#第一阶段:OS 层的“觉醒” (
java_start)) - [第二阶段:JVM 核心整备 (`JavaThread::run`)](#第二阶段:JVM 核心整备 (
JavaThread::run)) - [第三阶段:寻找 Java 入口 (`thread_main_inner`)](#第三阶段:寻找 Java 入口 (
thread_main_inner)) - 第四阶段:跃迁跳板 (`JavaCalls` 与 `call_stub`)
- [第五阶段:汇编级跃迁------`call_stub` 内部发生了什么?](#第五阶段:汇编级跃迁——
call_stub内部发生了什么?) - 总结:从死到生的瞬间
- [第一阶段:OS 层的"觉醒" (`java_start`)](#第一阶段:OS 层的“觉醒” (
前言
本文旨在记录近期研读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++ 代码。它必须先完成"身份认证"。
- 关键动作 :
- 设置 TLS (Thread Local Storage) :通过 TLS(线程本地存储)将当前的
JavaThread*存储在 CPU 寄存器(x86-64 下通常是R15)中,确保线程能随时找回自己的 JVM 句柄。 - 同步握手 :此时父线程可能还在
JVM_StartThread中等待,新线程会通过信号量通知父线程:"我已经成功出生,可以继续了"。 - 跃入 JVM 核心 :调用
thread->run()。
- 设置 TLS (Thread Local Storage) :通过 TLS(线程本地存储)将当前的
第二阶段:JVM 核心整备 (JavaThread::run)
此时,线程虽然还在 C++ 环境中,但已经开始按照 JVM 的规矩办事。
- 源码位置 :
src/share/vm/runtime/thread.cpp - 职责:配置线程的"生存环境"。
- 关键动作 :
- 栈警戒页 (Stack Guard Pages) :调用
os::create_stack_guard_pages(),通过mprotect系统调用在物理栈底划出 Yellow Page 和 Red Page 。这是StackOverflowError能够被安全捕获的底层硬件基础。 - 状态切换 :将线程状态从
_thread_new切换为_thread_in_vm。 - 调用
thread_main_inner():进入最终的调度中枢。
- 栈警戒页 (Stack Guard Pages) :调用
第三阶段:寻找 Java 入口 (thread_main_inner)
在 thread_main_inner 中,JVM 需要找到那个在 Java 层定义的 public void run()。
- 逻辑流转 :
- JVM 访问 Java 层的
Thread对象(通过eetop找到的关联对象)。 - 获取
run()方法的Method*指针。 - 核心调用 :执行
JavaCalls::call_virtual。
- JVM 访问 Java 层的
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 所在的内存区域时,它完成了一场物理层面的环境重塑:
- 保存 C++ 现场 :将当前 C++ 环境的寄存器(如
RBP,RBX,R12-R15)压入系统栈。 - 重置栈指针 (
RSP) :call_stub会计算 Java 方法所需的栈空间。- 它会执行
mov和sub指令,移动RSP到一个新的位置,这个位置之上是 C++ 栈,之下则是崭新的 Java 栈。
- 参数对齐 :将 Java 方法需要的参数(如
this指针,即Thread对象)从 C++ 的寄存器或栈位置拷贝到 Java 寄存器调用约定(如RSI,RDX等)或 Java 栈帧中。 - 设置 Anchor (锚点) :在
JavaFrameAnchor中标记当前的栈顶位置,这是为了后续 GC 能够准确回溯栈帧。 - 终极跳转 (
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 能够实现跨语言的异常传递和混合栈回溯。