揭秘JVM创世过程之Java线程和OS线程-灵魂与肉体

张开发
2026/4/11 14:27:10 15 分钟阅读

分享文章

揭秘JVM创世过程之Java线程和OS线程-灵魂与肉体
前言本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限文中内容难免存在疏漏恳请读者不吝指正。开篇在前几篇文章中我们从JVM的创建过程开始逐步深入探讨了底层汇编如何实现JVM的构建。本文将聚焦于Java线程与操作系统线程的关系。Java线程和OS线程-灵魂与肉体在 OpenJDK 8u44 的语境下Java 线程与系统线程的关系可以用一句话概括“1:1 映射的影子模型”。这意味着你在 Java 代码里new Thread()并调用start()时JVM 会真实地向操作系统申请创建一个原生的内核线程。为了管理这个复杂的对应关系HotSpot 设计了一个三层套娃的结构。1. 三层套娃数据结构层级在源码中一个“Java 线程”实际上由三个紧密耦合的对象组成层次源码对象职责Java 层java.lang.Thread开发者可见的对象。通过 eetop 字段持有底层 JavaThread 的地址。JVM 层JavaThread(C)最核心的管家。维护线程状态、HandleMark、栈锚点Anchor以及 JNI 句柄。系统层OSThread(C)平台相关的包装器保存了操作系统的Thread ID (TID)和句柄。关键连接点在java.lang.Thread类中有一个private volatile long eetop;字段。在 JVM 看来这个eetop存储的就是 C 层JavaThread对象的内存地址指针。2. 线程创建的源码追踪 (以 Linux 为例)线程的“出生证明”让我们顺着thread.start()的调用链看一看 OpenJDK 8u44 是如何一步步创建系统线程的第一步进入 JVM 入口 (jvm.cpp)Java 层的start0()是一个 native 方法对应 JVM 中的JVM_StartThread// src/share/vm/prims/jvm.cppJVM_ENTRY(void,JVM_StartThread(JNIEnv*env,jobject jthread))...// 1. 创建 C 层的 JavaThread 对象native_threadnewJavaThread(thread_entry,sz);...JVM_END第二步创建 JVM 管家 (thread.cpp)在JavaThread的构造函数中它会调用平台相关的os::create_thread向操作系统申请资源向操作系统申请资源// src/share/vm/runtime/thread.cppJavaThread::JavaThread(ThreadFunction entry_point,size_t stack_sz){...// 2. 调用平台相关的创建逻辑2. 调用平台相关的创建逻辑os::create_thread(this,thr_type,stack_sz);...}第三步调用系统内核 (os_linux.cpp)在 Linux 平台下JVM 最终会调用POSIX 线程库中著名的pthread_create// src/os/linux/vm/os_linux.cppboolos::create_thread(Thread*thread,ThreadType thr_type,size_t stack_size){...// 3. 真正的 Linux 系统调用。这里的 tid 是 Linux 系统的物理线程 IDintretpthread_create(tid,attr,thread_native_entry,thread);...}关键点pthread_create之后操作系统会分配一个LWP轻量级进程。此时Java 线程才真正拥有了执行权。3. 栈空间的关系谁分配了内存这是最容易产生误解的地方Java 线程的栈究竟在哪。很多人误以为 Java 线程栈在 Java 堆Heap里这是错误的。分配者在 JDK 8 中当你创建一个线程时栈空间是由操作系统通过mmap或malloc分配的而不是由 JVM 堆分配的。大小控制-Xss参数控制的就是这个系统栈的大小默认通常是1MB。物理联系系统线程的RSP栈顶指针直接指向这块内存。当Call Stub执行汇编指令时它就是在操作这块由操作系统分配的物理内存。关于Java线程栈与操作系统线程栈的关系推荐阅读这篇详细解析揭秘JVM创世过程之Java线程栈真相。4. 线程状态的同步谁在调度在 1:1 模型下JVM 不负责线程调度。调度权交给 KernelJava 线程的优先级、时间片分配、抢占全部由 Linux 的CFS完全公平调度器负责。状态映射当你在 Java 里看到线程状态是RUNNABLE时它在内核里可能是TASK_RUNNING。当你在 Java 里调用Thread.sleep()JVM 会调用os::sleep最终通过系统调用如nanosleep让内核线程进入不可调度的等待队列。5. 为什么 JDK 8 坚持 1:1 模型在 JDK 8 的时代1:1 模型是性能与复杂度的最佳平衡点优点利用多核直接交给操作系统调度可以完美利用多核 CPU 的并行能力。简化实现JVM 不需要自己写线程调度器如时间片分配、优先级抢占这些苦活累活全都丢给内核Kernel去处理。阻塞简单当一个 Java 线程发起 I/O 阻塞时操作系统会自动挂起对应的内核线程不会影响其他 Java 线程的运行。缺点也是虚拟线程诞生的原因内存昂贵每个线程至少 1MB 的栈限制了并发数上万个线程就可能 OOM。切换成本高线程上下文切换涉及用户态到内核态的转换非常消耗 CPU 周期。总结影子关系在 JDK 8 里Java 线程就像是内核线程的一个**“高级代理”**动作同步Java 线程sleep系统线程跟着sleep。生命共生死系统线程因为 OOM 崩溃Java 线程也就彻底消失。开销沉重因为 1:1 的存在你不能无限制地创建 Java 线程因为操作系统承受不了数万个内核线程频繁切换带来的压力。

更多文章