Java线程生命周期与上下文切换的本质
-
- 前言
- Java线程生命周期与上下文切换的本质
-
- [第一阶段:Java 世界的逻辑封包](#第一阶段:Java 世界的逻辑封包)
- [第二阶段:JVM 世界的资源编排](#第二阶段:JVM 世界的资源编排)
-
- [1. JNI 映射与准入](#1. JNI 映射与准入)
- [2. C++ JavaThread 对象的诞生](#2. C++ JavaThread 对象的诞生)
- 第三阶段:操作系统适配层 (OS Abstraction Layer)
-
- [1. 调用 `os::create_thread`](#1. 调用
os::create_thread) - [2. 握手 `Pthreads` 库](#2. 握手
Pthreads库)
- [1. 调用 `os::create_thread`](#1. 调用
- 第四阶段:从用户态到内核态的"致命一跃"
-
- [1. 触发 `clone()` 系统调用](#1. 触发
clone()系统调用) - [2. 进入内核:`do_fork` 与 `copy_process`](#2. 进入内核:
do_fork与copy_process)
- [1. 触发 `clone()` 系统调用](#1. 触发
- [第五阶段:反向激活(Callback & Stub)](#第五阶段:反向激活(Callback & Stub))
- 总结:完整转换链路清单
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
Java线程生命周期与上下文切换的本质
在 OpenJDK 8u44 的架构中,从一行简单的 Java 代码到 Linux 内核的任务调度,是一场跨越了四个世界的"接力赛":Java 虚拟机(JVM) 、本地运行库(C-Runtime/Pthreads) 、系统调用接口(System Call Interface) 以及 Linux 内核(Kernel)。
下面我们以 Thread.start() 为例,深度解构这条完整的转换链路。
第一阶段:Java 世界的逻辑封包
这一切始于 java.lang.Thread 类的堆内存分配。
- Java 源码 :
jdk/src/share/classes/java/lang/Thread.java - 动作 :
new Thread()仅仅是在 JVM 堆中创建了一个普通对象,并初始化了priority、daemon等属性。真正的转换发生在执行start()方法时。 - 关键点 :
start()内部调用了native void start0()。这是 JVM 预留的"虫洞",通过 JNI 指向了 C++ 实现。
第二阶段:JVM 世界的资源编排
控制权通过 JNI 进入了 HotSpot 的 C++ 环境。
1. JNI 映射与准入
- 源码 :
hotspot/src/share/vm/prims/jvm.cpp - 入口 :
JVM_StartThread。 - 职责 :JVM 检查
eetop(End-to-End Thread Object Pointer)是否已存在,确保一个 Java 线程不会被启动两次。
2. C++ JavaThread 对象的诞生
- 操作 :执行
new JavaThread(&thread_entry, sz)。 - 原理 :这会调用重写过的
operator new,在 Native Heap 而非 Java Heap 分配内存。此时,JVM 并没有直接呼叫内核,而是先在用户态构建好一个"管理实体"。
第三阶段:操作系统适配层 (OS Abstraction Layer)
JVM 需要跨越到系统平台相关的代码,将"逻辑线程"转化为"物理线程"。
1. 调用 os::create_thread
- 源码 :
hotspot/src/os/linux/vm/os_linux.cpp - 动作 :
- 分配
OSThread对象,用于记录 Linux 特有的属性(如 TID)。 - 设置线程栈大小。如果是 Java 线程,它会根据
-Xss或ThreadStackSize来计算字节数。
- 分配
2. 握手 Pthreads 库
- 核心调用 :执行
pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread)。 - 专家视点 :此时 JVM 调用的是 Linux 上的标准线程库(Glibc)。
java_start是 JVM 传入的回调函数,它就像是一个"驻内核办事处",负责在新线程启动后立即接管控制权。
第四阶段:从用户态到内核态的"致命一跃"
这是最底层的转换:pthread_create 并不是终点,它只是一个包装器。
1. 触发 clone() 系统调用
在 Linux 中,线程被视为轻量级进程(LWP) 。pthread_create 最终会触发 clone 系统调用(System Call #56)。
| 转换维度 | 用户态 (User Space) | 内核态 (Kernel Space) |
|---|---|---|
| 执行流 | 线程(Thread) | 任务(Task / LWP) |
| 内存结构 | JavaThread C++ 对象 |
task_struct 结构体 |
| 标识符 | Java 线程 ID | PID (内核视角下的进程 ID) |
| 栈控制 | JVM 管理的 1MB 虚拟空间 | 内核管理的 thread_info 和内核栈 |
2. 进入内核:do_fork 与 copy_process
内核接收到 clone 请求后:
- 分配 PID:为新线程分配一个唯一的进程标识符。
- 写时复制(COW):因为是线程,内核会让新任务共享父进程的地址空间、文件描述符和信号处理。
- 调度器入队 :新创建的
task_struct被放入 CPU 的运行队列(Runqueue)。
第五阶段:反向激活(Callback & Stub)
一旦内核调度器决定运行这个新任务,CPU 会跳转到之前注册的 java_start 地址。
- 物理就绪 :
java_start被触发,新线程在 Linux 层面正式存活。 - 同步屏障 :子线程在
os_linux.cpp中通过futex等待父线程完成eetop的绑定。 - 汇编跳板 :执行
JavaCalls::call_virtual->call_stub。 - 业务运行 :通过一段动态生成的汇编机器码,CPU 从 C++ 的
call_stub彻底"跳入" Java 字节码的run()方法。
总结:完整转换链路清单
Java 层 (
Thread.start)
→ \rightarrow → JNI 层 (JVM_StartThread)
→ \rightarrow → JVM 层 (JavaThread&OSThread)
→ \rightarrow → C 库层 (pthread_create)
→ \rightarrow → 系统调用层 (clone)
→ \rightarrow → 内核层 (task_struct& 调度器)
→ \rightarrow → 反向回调 (java_start)
→ \rightarrow → 汇编跳板 (call_stub)
→ \rightarrow → 代码运行 (run)