前情提要,Stop-The-World 简称 STW,是指在执行垃圾回收的过程冻结所有用户线程的运行,直到垃圾回收线程执行结束。那么,用户线程是如何阻塞呢?这里将会涉及到一个概念 SafePoint。
为什么要 Stop-The-World
目前主流的虚拟机采用的都是可达性算法,算法的核心是利用根对象作为起始点,根据对象之间的引用关系,即引用链,通过遍历引用链来判断对象的是否存活。
然而,可达性分析算法要求全过程都基于一个能保障一致性的快照中才能够进行分析,简单来说,就是必须全程冻结用户线程的运行。
为什么必须冻结用户线程呢?
因为,如果用户线程和垃圾回收线程并发执行,有可能会出现两个问题:浮动垃圾 和 对象消失,详情参考 并发的可达性分析。
什么是 SafePoint
SafePoint 就是一个安全点,可以理解为用户线程执行过程中的一些特殊位置。SafePoint 保存了当前线程的 上下文,当线程执行到这些位置的时候,说明线程当前的状态是 确定的,线程有哪些对象、使用了哪些内存。
因此,只有用户线程处于 SafePoint 的时候,线程才可以安全阻塞。
这意味着,Stop-The-World 需要所有的用户线程处于 SafePoint。
SafePoint 在哪些位置
SafePoint 插入到代码的某些位置,线程运行到 SafePoint 代码时,将会主动去检查是否需要进入 SafePoint,这个主动检查的过程,被称为 Polling。
◉ 所有的非计数循环的末尾
(防止循环体的执行时间太长,一直进入不了 SafePoint)
◉ 所有方法返回之前
◉ 每条 Java 编译后的字节码的边界
SafePoint 的数量不能太少,因为这将会导致进入 SafePoint 的前置时间过长,以至于垃圾回收线程等待的时间太长。
SafePoint 的数量也不能太多,过于频繁的 Pollong 会有性能损耗。
如何实现 Stop-The-World
首先,Stop-The-World 需要所有的用户线程处于 SafePoint,这意味着某个用户线程运行到 SafePoint,其它用户线程可能处于不同的状态。
所有线程都到达GC Safepoint,有两种方法:
◉ 抢占式中断(Preemptive Suspension)
JVM会中断所有线程,然后依次检查每个线程中断的位置是否为Safepoint,如果不是则恢复用户线程,让它执行至 Safepoint 再阻塞。
◉ 主动式中断(Voluntary Suspension)
大部分 JVM 实现都是采用主动式中断,需要阻塞用户线程的时候,首先做一个标志,用户线程会主动轮询这个标志位,如果标志位处于就绪状态,就自行中断。
那么,针对用户线程的各种状态,需要怎么处理呢?
◉ 正在执行字节码
解释器检查当前用户线程的标志,VM 线程调用以下方法阻塞线程。
Interpreter::notice_safepoints()
◉ 正在运行 native 代码
如果 VM线程 发现一个当前用户线程正在执行 native 代码,不会等待线程阻塞。当线程从 native 代码返回时,检查 Safepoint 状态,如果处于 SafePoint,就直接阻塞线程。
◉ 正在运行 JIT 编译好的代码
设置 Polling Page 为不可读,当前用户线程发现该内存页不可读时,就会被阻塞。
Poling Page:
在 JVM 初始化启动的时候,初始化的一个单独的内存页面,这个页面是让运行的编译过的代码的线程进入阻塞状态的关键,是一个全局的 Safepoint Polling Page。
◉ 处于阻塞状态
在所有其他用户线程进入 SafePoint 之前,一直阻塞当前用户线程。
◉ 处于线程切换状态
一直轮询该用户线程状态,直到线程处于阻塞状态。
再说几句
既然 Stop-The-World 需要所有的用户线程处于 SafePoint,那么是否可以进一步进行优化,减少所有用户线程进入 SafePoint 的前置时间,更快的开始执行垃圾回收线程?