关于safepoint
通过JITWatch查看汇编代码,观测jdk17改动对代码的影响
前言
相信大家都听说过System.gc()
不会触发立马进行gc,但是不太知道原因;相信大家也听说过gc的时候会STW,暂停所有的业务线程,应该是为了标记垃圾。
- 实际上
System.gc()
不会立马执行,是因为需要用户线程都达到safepoint; - 用户线程都达到safepoint之后,会根据每个线程的oopMap,标记还在使用的堆内存,没有使用的堆内存就是待清理的垃圾;也就是STW的过程
一、safepoint是什么?
safepoint的执行流程:
- vm thread通知:java thread需要陷入到safepoint;
- java thread通过执行到
轮询safepoint
的指令而陷入safepoint - vm thread通过loop,发现所有java thread陷入safepoint之后,进行后续处理
- vm Thread执行完自己的逻辑之后,通知java Thread返回到之前的状态继续执行
1.1. 哪些场景jvm会通知java Thread
那还有哪些情况jvm会通知java thread 陷入safepoint呢?
gc 逆优化(jit一般是进行激进优化,但是有时候遇到分支预测失败,只能逆优化,使用解释执行) 偏向锁撤销 类相关元数据操作 周期性,受GuaranteedSafepointInterval参数控制,默认是1秒
也就是上面这几种场景 vm Thread会通知java Thread陷入safepoint
1.2. 通知了之后,不同状态的java Thread会如何处理
java
// Begin the process of bringing the system to a safepoint.
// Java threads can be in several different states and are
// stopped by different mechanisms:
//
// 1. Running interpreted
// The interpeter dispatch table is changed to force it to
// check for a safepoint condition between bytecodes.
// 2. Running in native code
// When returning from the native code, a Java thread must check
// the safepoint _state to see if we must block. If the
// VM thread sees a Java thread in native, it does
// not wait for this thread to block. The order of the memory
// writes and reads of both the safepoint state and the Java
// threads state is critical. In order to guarantee that the
// memory writes are serialized with respect to each other,
// the VM thread issues a memory barrier instruction
// (on MP systems). In order to avoid the overhead of issuing
// a memory barrier for each Java thread making native calls, each Java
// thread performs a write to a single memory page after changing
// the thread state. The VM thread performs a sequence of
// mprotect OS calls which forces all previous writes from all
// Java threads to be serialized. This is done in the
// os::serialize_thread_states() call. This has proven to be
// much more efficient than executing a membar instruction
// on every call to native code.
// 3. Running compiled Code
// Compiled code reads a global (Safepoint Polling) page that
// is set to fault if we are trying to get to a safepoint.
// 4. Blocked
// A thread which is blocked will not be allowed to return from the
// block condition until the safepoint operation is complete.
// 5. In VM or Transitioning between states
// If a Java thread is currently running in the VM or transitioning
// between states, the safepointing code will wait for the thread to
// block itself when it attempts transitions to a new state.
//
以下是豆包给我的翻译
java
开始将系统带入安全点的过程。Java 线程可能处于多种不同状态,通过不同机制被暂停:
1. 运行解释执行代码
解释器分发表会被修改,强制其在字节码之间检查安全点条件。
2. 运行本地代码(Native Code)
从本地代码返回时,Java 线程必须检查安全点_state,判断是否需要阻塞。如果 VM 线程(虚拟机线程)发现某个 Java 线程正处于本地代码执行状态,则不会等待该线程阻塞。安全点状态与 Java 线程状态的内存读写顺序至关重要。为确保内存写操作之间的序列化,VM 线程会(在多处理器系统上)发出内存屏障指令。为避免每个 Java 线程执行本地调用时都要触发内存屏障的开销,每个 Java 线程在修改线程状态后,会对单个内存页执行写操作。VM 线程通过执行一系列 mprotect 系统调用来强制所有 Java 线程之前的写操作完成序列化,这一过程在 os::serialize_thread_states () 调用中实现。事实证明,这比在每次本地调用时执行内存屏障指令高效得多。
3. 运行编译后代码
编译后代码会读取一个全局(安全点轮询)页,当我们尝试进入安全点时,该页会被设置为触发页错误(fault)。
4. 阻塞状态
处于阻塞状态的线程在安全点操作完成前,不允许从阻塞条件中返回。
5. 在 VM 中执行或状态转换中
如果 Java 线程当前正在 VM 中执行,或处于状态转换过程中,安全点代码会等待该线程在尝试转换到新状态时自行阻塞。
- 对于字节码,各位javaer应该都很熟,就是javac前置编译之后的jvm指令,当不使用jit编译时,就只能使用解释器进行逐行解释执行,没有热点代码,执行效率较低。强制其在字节码之间检查安全点条件。(也就是判断是否需要进入安全点)
- 本地代码的话,其实就是java那些标志了native的方法,native方法的执行不受jvm控制,每次返回之前都需要进行安全点状态判断(判断是否需要等待vm的safepoint操作完成)
- 运行编译后代码,其实就是我们的jit编译之后的代码了,可以参考再探volatile原理里我们的代码注释,发现每次循环之后给加了安全点轮询(判断是否需要进入safepoint)
- 阻塞状态可以理解为等待某个资源的线程,在等待的资源满足之后,也不能简单的直接状态变更,需要进行安全点状态判断(判断是否需要vm的safepoint操作完成)
- 如果线程本身就是在vm中执行(非用户业务逻辑)或者线程状态切换时,线程去检查是否需要进入safepoint操作完成,如果需要,就把自己给阻塞
对于咱们程序员来说,1、2、3、4是比较常见的,写代码可能需要注意的场景
1.3. 哪些地方插入safepoint轮询的代码
此处引用下别人的文章:JVM源码分析之安全点safepoint
解释器执行的字节码: 理论上,在解释器的每条字节码的边界都可以放一个safepoint,不过挂在safepoint的调试符号信息要占用内存空间,如果每条机器码后面都加safepoint的话,需要保存大量的运行时数据,所以要尽量少放置safepoint,在safepoint会生成polling代码询问VM是否要"进入safepoint",polling操作也是有开销的,polling操作会在后续解释。
JIT执行的汇编代码: 通过JIT编译的代码里,会在所有方法的返回之前,以及所有非counted loop的循环(无界循环)回跳之前放置一个safepoint,为了防止发生GC需要STW时,该线程一直不能暂停。另外,JIT编译器在生成机器码的同时会为每个safepoint生成一些"调试符号信息",为GC生成的符号信息是OopMap,指出栈上和寄存器里哪里有GC管理的指针。
二、源码分析
此处源码分析是基于 jdk8u192-b12
2.1.VMThread
create: create方法里会初始化队列_vm_queue execute: 该方法执行时,会往_vm_queue里放入一个任务,譬如gc线程就是通过执行该方法往_vm_queue放入一个operation run :线程的主要方法,该方法的主要逻辑就是执行loop方法 loop:loop方法,顾名思义,就是不停循环:不停从_vm_queue里poll任务,然后执行。如果判断执行某个operation之前需要执行safepoint操作,就会触发 下面是loop方法执行的主逻辑:
markdown
1. 这是 VM 线程的主循环,负责处理所有 VM 操作(如垃圾回收、类加载等)
2. 通过队列机制获取并执行 VM 操作
3. 处理两种类型的操作:需要安全点(Safepoint)的操作和不需要安全点的操作
4. 确保定期进入安全点,以支持需要全局同步的操作
5. 包含超时处理、自毁机制和性能跟踪等辅助功能
2.2. SafepointSynchronize
begin:通知以及等待线程陷入到safepoint状态 主要流程如下:
ini
1. 各种加锁,将_state = _synchronizing;
2. 执行OrderAccess::fence(),也就是内存屏障。使得所有线程的状态和数据刷回到主内存,也让所有线程读取最新的_state值;如果没有使用内存屏障,就通过os::serialize_thread_states()方法
3. Interpreter::notice_safepoints(); 通知解释器进行safepoint的检查
4. os::make_polling_page_unreadable(); 如果使用的编译后代码,就将polling_page设置为不可读
5. 进入循环,扫描所有的javaThread:
5.1 分析线程当前状态(如是否在解释执行、编译执行、native调用等),
5.2 依据不同状态决定如何让线程暂停(如修改解释器表、触发缺页异常等)
5.3 如果遇到counted loop等暂时没法陷入safepoint的线程,当前os线程就会进行自旋等待
6. _state = _synchronized;
7. 执行OrderAccess::fence(),让所有线程读取最新的_state值
关于polling_page
不可读是如何生效的,可以看看再探volatile原理里的汇编代码,里面有一句ldr wzr, [x10]
,就是读取的polling_page
里的数据,如果该地址不可读,那么就会触发异常。 开启了safepoint之后,通过evaluate_operation(_cur_vm_operation);
执行当前的vm operation;然后还有个优化,如果_vm_queue
里后续operation也都是safepoint的operation,就都poll
出来执行
end: 状态清理 主要流程如下:
css
8. 执行os::make_polling_page_readable(),使得线程不再会因为执行polling_page的读操作而陷入safepoint
9. Interpreter::ignore_safepoints(),解释执行也不再检查safepoint
10._state = _not_synchronized,更改线程状态
10. 执行OrderAccess::fence(),让所有线程读取最新的_state值
11. 进入循环,扫描所有的javaThread
12.1: cur_state->restart(); // 重启线程
handle_polling_page_exception: 调用ThreadSafepointState::handle_polling_page_exception()
得以实现 -> 处理的是当polling_page是不可读状态,而线程却对polling_page进行读操作,之后的异常处理操作。
markdown
12. 如果需要返回OOP,那么需要先把当前safepoint的oop进行保存:将oop和寄存器结果一起保存到栈上,等待线程restart时进行读取
13. 执行SafepointSynchronize::block(thread())将线程进行block
14. 从block状态返回的时候,进行oop和寄存器结果读取
oop其实就是普通对象引用,针对编译执行,会在编译时,就记录oop的位置到oopMap中,当陷入到safepoint时,vm Thread就可以从oopMap中通过oop的位置找到oop。 可以发现,如果进行GC的话,需要在STW期间,通过GC ROOTS查找还在使用的对象。其实STW期间,利用了safepoint时,不同线程绑定的oopMap来查找还在被使用的oop,组成了GC ROOTS的一部分
总结
再次深入理解了下safepoint的作用:包括如何通知java Thread陷入到safepoint,以及不同状态的线程如何陷入safepoint;同时附带了一些源码解读