我们先来设想下如下场景:
- 当需要 GC 时,需要知道哪些对象还被使用,或者已经不被使用可以回收了,这样就需要每个线程的对象使用情况。
- 对于偏向锁(Biased Lock),在高并发时想要解除偏置,需要线程状态还有获取锁的线程的精确信息。
- 对方法进行即时编译优化(OSR栈上替换),或者反优化(bailout栈上反优化),这需要线程究竟运行到方法的哪里的信息。
对于这些操作,都需要线程的各种信息,例如寄存器中到底有啥,堆使用信息以及栈方法代码信息等等等等,并且做这些操作的时候,线程需要暂停,等到这些操作完成,否则会有并发问题。这就需要 SafePoint。
Safepoint 可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,线程可以暂停。在 SafePoint 保存了其他位置没有的一些当前线程的运行信息,供其他线程读取。这些信息包括:线程上下文的任何信息,例如对象或者非对象的内部指针等等。我们一般这么理解 SafePoint,就是线程只有运行到了 SafePoint 的位置,他的一切状态信息,才是确定的,也只有这个时候,才知道这个线程用了哪些内存,没有用哪些;并且,只有线程处于 SafePoint 位置,这时候对 JVM 的堆栈信息进行修改,例如回收某一部分不用的内存,线程才会感知到,之后继续运行,每个线程都有一份自己的内存使用快照,这时候其他线程对于内存使用的修改,线程就不知道了,只有再进行到 SafePoint 的时候,才会感知。
所以,GC 一定需要所有线程同时进入 SafePoint,并停留在那里,等待 GC 处理完内存,再让所有线程继续执。像这种所有线程进入 SafePoint等待的情况,就是 Stop the world
为什么需要 SafePoint 以及 Stop The World?
在 SafePoint 位置保存了线程上下文中的任何东西,包括对象,指向对象或非对象的内部指针,在线程处于 SafePoint 的时候,对这些信息进行修改,线程才能感知到。所以,只有线程处于 SafePoint 的时候,才能针对线程使用的内存进行 GC,以及改变正在执行的代码,例如 OSR (On Stack Replacement,栈上替换现有代码为JIT优化过的代码)或者 Bailout(栈上替换JIT过优化代码为去优化的代码)。并且,还有一个重要的 Java 线程特性也是基于 SafePoint 实现的,那就是 Thread.interrupt()
,线程只有运行到 SafePoint 才知道是否 interrupted。
为啥需要 Stop The World,有时候我们需要全局所有线程进入 SafePoint 这样才能统计出那些内存还可以回收用于 GC,,以及回收不再使用的代码清理 CodeCache,以及执行某些 Java instrument 命令或者 JDK 工具,例如 jstack 打印堆栈就需要 Stop the world 获取当前所有线程快照。
SafePoint 如何实现的?
可以这么理解,SafePoint 可以插入到代码的某些位置,每个线程运行到 SafePoint 代码时,主动去检查是否需要进入 SafePoint,这个主动检查的过程,被称为 Polling
理论上,可以在每条 Java 编译后的字节码的边界,都放一个检查 Safepoint 的机器命令。线程执行到这里的时候,会执行 Polling 询问 JVM 是否需要进入 SafePoint,这个询问是会有性能损耗的,所以 JIT 会优化尽量减少 SafePoint。
经过 JIT 编译优化的代码,会在所有方法的返回之前,以及所有非counted loop的循环(无界循环)回跳之前放置一个 SafePoint,为了防止发生 GC 需要 Stop the world 时,该线程一直不能暂停,但是对于明确有界循环,为了减少 SafePoint,是不会在回跳之前放置一个 SafePoint,也就是:
for (int i = 0; i < 100000000; i++) { ... }
里面是不会放置 SafePoint 的,这也导致了后面会提到的一些性能优化的问题。注意,仅针对 int 有界循环,例如里面的 int i 换成 long i 就还是会有 SafePoint;
SafePoint 实现相关源代码:safepoint.cpp
可以看出,针对 SafePoint,线程有 5 种情况;假设现在有一个操作触发了某个 VM 线程所有线程需要进入 SafePoint(例如现在需要 GC),如果其他线程现在:
- 运行字节码:运行字节码时,解释器会看线程是否被标记为 poll armed,如果是,VM 线程调用
SafepointSynchronize::block(JavaThread *thread)
进行 block。 - 运行 native 代码:当运行 native 代码时,VM 线程略过这个线程,但是给这个线程设置 poll armed,让它在执行完 native 代码之后,它会检查是否 poll armed,如果还需要停在 SafePoint,则直接 block。
- 运行 JIT 编译好的代码:由于运行的是编译好的机器码,直接查看本地 local polling page 是否为脏,如果为脏则需要 block。这个特性是在 Java 10 引入的 JEP 312: Thread-Local Handshakes 之后,才是只用检查本地 local polling page 是否为脏就可以了。
- 处于 BLOCK 状态:在需要所有线程需要进入 SafePoint 的操作完成之前,不许离开 BLOCK 状态
- 处于线程切换状态或者处于 VM 运行状态:会一直轮询线程状态直到线程处于阻塞状态(线程肯定会变成上面说的那四种状态,变成哪个都会 block 住)。