揭秘Java世界中safepoint之调用过程和生命周期解析
调用过程和生命周期解析
- 前言
- safepoint的调用过程和生命周期解析
- 一、 Safepoint 的生命周期
- 二、 核心源码剖析
- 1. 触发与同步阶段:`SafepointSynchronize::begin()`
- 2. 线程如何感知并响应 Safepoint?
- 3. 恢复与结束阶段:`SafepointSynchronize::end()`
- 三、 典型调用栈分析
- 1. VMThread 侧(发起与等待栈)
- 2. JavaThread 侧(JIT 编译代码触发 Page Fault 挂起栈)
- 四、 核心原理总结
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
safepoint的调用过程和生命周期解析
作为 Java 系统工程师,理解Safepoint(安全点)的底层源码与生命周期是调优 JVM 性能、排查 STW(Stop-The-World)大暂停的必修课。
在 OpenJDK 8中,Safepoint 的核心逻辑主要由SafepointSynchronize类(位于share/vm/runtime/safepoint.cpp)驱动。下面我们结合 OpenJDK 8源码,深入拆解 Safepoint 的生命周期、核心调用栈以及关键源码实现。
一、 Safepoint 的生命周期
一个完整的 Safepoint 从触发到结束,会经历以下 4 个核心阶段:
[运行状态] │ ▼ (1. 发起) VMThread 调用 begin(),设置状态为 _synchronizing [同步阶段] ─── JavaThread 到达安全点(轮询页置脏 / 解释器检测) │ ▼ (2. 达标) 所有 JavaThread 暂停,状态变为 _synchronized [执行阶段] ─── 执行 GC、Biased Lock Revocation、Class Redefinition 等 STW 任务 │ ▼ (3. 恢复) VMThread 执行 end(),重置状态为 _not_synchronized [唤醒阶段] ─── 恢复全局轮询页,JavaThread 离开 Safepoint 继续执行二、 核心源码剖析
1. 触发与同步阶段:SafepointSynchronize::begin()
当VMThread(负责执行 JVM 内部安全任务的线程)收到一个需要 STW 的请求时,会调用begin()方法。这是拉开 Safepoint 序幕的核心函数。
// share/vm/runtime/safepoint.cppvoidSafepointSynchronize::begin(){assert(Thread::current()->is_VM_thread(),"Only VM thread can execute a safepoint");// 1. 更新全局安全点计数器_safepoint_counter++;// 2. 将安全点状态从 _not_synchronized(未同步)设置为 _synchronizing(正在同步)_state=_synchronizing;OrderAccess::fence();// 确保内存屏障,让其他线程立即可见// 3. 关键:将全局安全点轮询页面(Polling Page)设为不可读/不可写(Mprotect 触发 SIGSEGV)// 编译形(JITed)代码在运行时会定期读取这个页面,一旦变脏,立刻触发异常进入信号处理程序os::make_polling_page_unreadable();// 4. 等待所有运行中的 Java 线程进入安全点intiterations=0;intactive_threads=0;while(true){// 检查是否所有线程都已经到达安全点// 根据线程当前的状态(如 _thread_in_native, _thread_in_Java)来判断if(can_is_security_check_pass(&active_threads)){break;// 所有线程已安全暂停,跳出循环}// 如果耗时过长,可能会触发 SafepointTimeout 警告if(SafepointTimeout&&!was_timeout&&(iterations==SafepointTimeoutDelay)){rc_with_timeout_mprotect();// 打印超时日志}// 适当让出 CPU 资源,等待其他线程响应轮询os::naked_yield();}// 5. 此时所有 Java 线程均已暂停,将状态修改为 _synchronized(已同步完成)_state=_synchronized;OrderAccess::fence();}2. 线程如何感知并响应 Safepoint?
Java 线程有不同的运行模式,JVM 对其进入 Safepoint 的判定方式也不同:
- 解释执行(Interpreter):
解释器在每条字节码执行前(或向后跳转、方法返回时)会检查一个全局的 Safepoint 地址。 - 编译执行(JIT Compiled):
JIT 编译器会在方法出口和循环回边(Loop Backedge)插入一句话:test %eax, 0x160000(读取轮询页)。当begin()中将该页置脏时,该指令触发硬件页错误,捕获后转入SafepointSynchronize::handle_polling_page_exception()。 - 本地代码(Native Code):
正在执行 JNI 方法的线程无需暂停,因为它们不会修改 Java 堆对象。但当它们准备从 JNI 返回 Java 空间时,会检查状态,若处于_synchronizing则会被阻塞。
下面是线程从 JNI 返回或者在安全点前被阻塞的核心代码:
// share/vm/runtime/safepoint.cpp// 当 JavaThread 发现系统处于同步状态,自愿调用此方法挂起自己voidSafepointSynchronize::block(JavaThread*thread){// 检查当前全局状态是否还是同步中/已同步if(_state!=_not_synchronized){// 改变当前线程状态,表明自己已经安全挂起// 这样 VMThread 就能在 can_is_security_check_pass() 中对本线程放行JavaThreadState saved_state=thread->thread_state();thread->set_thread_state(_thread_blocked);// 进入等待队列,挂起在 Safepoint_lock 锁上Safepoint_lock->lock_without_safepoint_check();// 循环等待,直到状态恢复为 _not_synchronizedwhile(_state!=_not_synchronized){Safepoint_lock->wait(Mutex::_no_safepoint_check_flag);}Safepoint_lock->unlock();// 被唤醒后,恢复原先的线程状态,继续执行 Java 代码thread->set_thread_state(saved_state);}}3. 恢复与结束阶段:SafepointSynchronize::end()
当 VMThread 顺利执行完 GC 或其他 STW 任务后,会调用end()唤醒所有阻塞的 Java 线程。
// share/vm/runtime/safepoint.cppvoidSafepointSynchronize::end(){assert(Thread::current()->is_VM_thread(),"Only VM thread can execute a safepoint");// 1. 恢复全局轮询页面为可读// JIT 编译代码再度执行到轮询点时,将恢复正常读取,不再触发异常os::make_polling_page_readable();Safepoint_lock->lock_without_safepoint_check();// 2. 将全局状态重置为 _not_synchronized_state=_not_synchronized;// 3. 关键:通知并唤醒所有因为安全点被 block 在 Safepoint_lock 上的 JavaThreadSafepoint_lock->notify_all();Safepoint_lock->unlock();// 4. 清理并开启下一轮垃圾回收/安全点周期的准备工作RuntimeService::record_safepoint_end();}三、 典型调用栈分析
当我们在做底层调优、或者由于 Safepoint 导致长暂停而观察jstack/pstack时,最常见的底层 C++ 调用栈如下:
1. VMThread 侧(发起与等待栈)
Thread 30517 (VM Thread): #0 pthread_cond_wait ... // 物理挂起,等待某些顽固线程就位 #1 os::PlatformEvent::park() #2 Monitor::ILock(Thread*) #3 Monitor::lock_without_safepoint_check() #4 SafepointSynchronize::begin() <-- 1. VMThread 触发安全点并进入同步循环 #5 VMThread::loop() <-- 2. VMThread 核心轮询事件队列 #6 VMThread::run() <-- 3. VM 线程启动2. JavaThread 侧(JIT 编译代码触发 Page Fault 挂起栈)
Thread 30511 (Java Thread_1): #0 pthread_cond_wait ... // 线程在此处被真正挂起(STW 发生中) #1 os::PlatformEvent::park() #2 Monitor::IWait(Thread*, long long) #3 Monitor::wait(bool, long, bool) #4 SafepointSynchronize::block(JavaThread*) <-- 1. 线程在此处将自己修改为 _thread_blocked 并 wait #5 SafepointSynchronize::handle_polling_page_exception(JavaThread*) <-- 2. 信号处理器捕获到 SIGSEGV,交给 JVM 处理 #6 <StubRoutines::_call_stub> <-- 3. 硬件层面的轮询指令 (test %eax, polling_page) 触发中断 #7 [Generated Method] <-- 4. 正在跑的 JIT 编译业务代码(如某个 controller 方法)四、 核心原理总结
- 硬件屏障巧妙利用 (
mprotect):OpenJDK 并没有在 JIT 代码中加入繁琐的if (safepoint_flag)分支判断(这会极大降低 CPU 流水线执行效率),而是直接使用一行test汇编指令去读一个固定的内存页。正常时该页可读,开销极小;需要安全点时,将其改为不可读,利用 CPU 硬件层面的页错误(Page Fault)异步阻断执行流,效率极高。 - 状态机判定:VMThread 不需要等所有线程都走到某一行代码,只要 JavaThread 的状态处于
_thread_in_native(执行本地方法)、_thread_in_vm、或者已被_thread_blocked(阻塞),VMThread 就认为该线程已经“安全”了,可以开始 STW 任务。 - TTSP (Time To Safepoint) 优化:如果业务代码中存在没有放置 Safepoint 的长循环(例如通过
Counted Loop优化的int循环),Java 线程迟迟无法读取轮询页,就会导致 TTSP 过长,从而使整个 JVM 陷入长时间的伪挂起。这在生产调优中通常需要通过-XX:+UseCountedLoopSafepoints来显式解决。
