先说一个真实经历。有次线上订单服务,没有任何GC报警,但接口平均耗时突然从20ms涨到3秒,持续了大概半分钟。第一时间抓线程dump,发现大量业务线程停在“等待安全点”相关的状态,而 "VM Thread" 正在runnable状态。那会儿我才真正意识到,平时只盯着GC日志是不够的,VMThread和它背后的安全点机制,才是JVM全局停顿真正的枢纽。这篇就把这两块完全讲透,适合正在排查线程卡顿、读JVM源码、或准备JVM面试的同学。
1. JVM线程模型里的隐藏角色:VMThread
1.1 VMThread到底是什么
先帮基础薄弱的读者理清三件套:JDK是开发工具包,JRE是Java运行环境,JVM是JRE里真正执行字节码的虚拟机。VMThread是JVM内部自己创建的线程,不需要Java代码参与。整个HotSpot里只存在一个VMThread实例,它的任务不是执行Java方法,而是去执行另一个东西:VM Operation。
HotSpot的线程族大概是这样的结构:
text复制HotSpot线程族
├── JavaThread(普通业务线程,跑Java代码)
├── VMThread(VM Operation专用执行线程)
├── GC Thread(若干GC线程)
├── Compiler Thread(JIT编译线程)
└── 其他非Java线程
大多数工程师对GC线程和编译器线程比较熟,VMThread反而常常被忽略,但它在线程dump里出现的次数并不少。这里还要注意“JVM线程”和“OS线程”的关系。HotSpot默认采用1:1线程模型,每个JavaThread底层对应一个原生OS线程,Java线程的调度完全交给操作系统。VMThread同样是原生线程,但它没有对应的Java Thread对象,不跑字节码,只跑C++实现的VM Operation。正因为这个特殊定位,它的栈通常是 VMThread::run、VMThread::evaluate_operation 这类JVM内部栈,而不是 java.lang.Thread.run。
1.2 VM Operation:VMThread要执行的“大活”
VM Operation是HotSpot内部定义的一种C++操作对象,统一派发给VMThread串行执行。哪些场景会用到?我列一下高频出现的:
- 部分GC触发,比如
G1CollectForAllocation、ParallelGCSystemGC - 堆转储
jmap -dump、类直方图jmap -histo - 偏向锁批量撤销和批量重偏向
BulkRevokeBias - JIT编译结果最终安装、
Deoptimize逆优化、ICBufferFull清空内联缓存 ThreadsSuspendAtPoll,也就是安全点轮询触发的线程挂起- JVMTI相关操作,比如重定义类、强制GC
- 线程dump、死锁检测等诊断操作
用生活化的说法:每个Java线程可以自由处理自己的事,但一些涉及全局状态的操作不可以在单个Java线程里乱改,需要把所有人“叫到一起”同步。VMThread就是那个主持人。它永远不执行普通代码,只是从VMOperationQueue队列里不断取操作,逐个执行,执行完再等下一个。如果你在面试中被问到“JVM有哪些线程模型”,除了回答Java线程和OS线程的映射关系,把VMThread、GC线程、编译器线程的存在讲清楚,往往更能体现对JVM的理解深度。
1.3 VMThread的工作循环
VMThread的生命周期很简单,看起来其实像一个事件循环:
- 线程启动后调用
VMThread::run()进入事件循环; - 获取
VMOperationQueue的锁,等待某个Java线程或者JVM内部模块提交操作; - 如果有操作且当前状态允许,就取出操作执行其
evaluate()方法; - 执行完以后通知等待该操作的线程,回到队列继续等。
为什么要单独用高优先级线程?我理解有两个原因。第一是串行化,全局一致性上只允许一个线程同时操
