暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

浅析 JVM Safepoint 以及 Stop The World

Just do DT 2021-11-18
1846

什么是 SafePoint?


safepoint,最简洁的定义是 “A point in program where the state of execution is known by the VM”。这里"state of execution" 特意说得模糊,是因为不同种类的 safepoint 需要的数据不一样。其中,GC safepoint 是最常见、大家听说得最多的,除此之外还有 deoptimization safepoint 。在 HotSpot VM 里面,这两种 safepoint 目前实现在一起,但其实概念上他们俩没有直接联系,需要的是数据不一样。
GC safepoint 需要知道在那个程序位置上,调用栈、寄存器等一些重要的数据区域里面什么地方包含了GC 管理的指针。
如果触发一次GC,那么 JVM 里面的所有 Java 线程都必须达到 GC safepoint,不同的 JVM 实现会选用不同位置放置 safepoint,以 HotSpot JVM 为例,在解释器里每条字节码的边界都可以是一个 safepoint,因为 HotSpot 的解释器总是很容易的找出完整的 “state of execution”。
而在 JIT 编译的代码里,HotSpot 会在所有方法的临返回之前,以及所有非 counted loop 的循环的回跳之前放置 safepoint。
HotSpot 的 JIT 编译器不但会生成机器码,还会额外在每个 safepoint 生成一些 “调试符号信息”,以便 VM 能找到所需要的 “state of execution”。为 GC 生成的符号信息是 OopMap,指出栈上和寄存器哪里有 GC 管理的指针。

为什么 safepoint 的位置只在特定的位置?


  • 挂在 safepoint 的调试符号信息要占用空间。如果允许每条机器码都可以是 safepoint 的话,需要存储的数据量会很大(当然这有办法解决,例如用 delta 存储和使用压缩)
  • safepoint 会影响优化。特别是 deoptimization safepoint,会迫使 JVM 保留一些只有解释器可能需要的、JIT 编译器认定无用的变量的值。本来 JIT 编译器可能可以发现某些值不需要而消除他们对应的运算。

什么是 Stop The World?


所有线程进入 SafePoint 等待的情况就是 Stop The World。比如,GC 时候一定需要所有线程同时进入 SafePoint,并停留在那里,等待 GC 处理完内存,再让所有线程继续执行。

为什么需要 SafePoint 以及 Stop The World?


在 Safepoint 位置保存了线程上下文中的任何东西,包括对象,指向对象或非对象的内部指针,在线程处于 Safepoint 的时候,对这些信息进行修改,线程才能感知到。所以,只有线程处于 Safepoint 的时候,才能针对线程使用的内存进行 GC,以及改变正在执行的代码。并且,还有一个重要的 Java 线程特性也是基于 Safepoint 实现的,那就是 Thread.interrup()
,线程只有运行到 Safepoint 才知道是否 interrupted
为啥需要 Stop The World,有时候我们需要全局所有线程进入 Safepoint 这样才能统计出那些内存可以回收用于 GC,以及回收不再使用的代码清理 CodeCache,以及执行某些 Java instrument 命令或者 JDK 工具,例如:jstack 打印堆栈就需要 Stop The World 获取当前所有线程快照。

Safepoint 如何实现的?


可以理解,Safepoint 可以插入到代码的某些位置,每个线程运行到 Safepoint 代码时,主动去检查是否需要进入 Safepoint,这个主动检查的过程,被称为 Polling



thread1 在收到设置 safepoint 之前是一直执行的,在收到信号之后还会执行一段时间,然后到达 Safepoint 暂停执行。
thread2 先执行了一段时间,然后因为 CPU 被抢夺,空闲了一段时间,在这段时间里面,thread2 收到了设置 safepoint 的信号,然后 thread2 获得执行权力,接着继续执行,最后到达 safepoint。
thread3 是一个 native 方法,将会一直执行,直到 safepoint结束。
thread4 也是一个 native 方法,它和 thread3 的区别就在于 thread4 在 safepoint 开始和结束之间结束了,需要将控制器转交给普通的 java 线程,因为这个时候 JVM 在执行 safepoint 的操作,所以任然需要暂停执行。
safepoint 暂停时间可以分为如下两类
  • 从发起安全点请求到所有线程到达安全点所用的时间
  • 执行安全点操作所需的时间

可以添加 -XX:+PrintGCApplicationStoppedTime 和 -XX:+PrintGCDetails 看见具体的时间,比如下面的案例

SafePoint 案例


案例1

package com.justdodt.jvm;


/**
* @Author:JustDoDT
* @Description:
* @Date:Create in 23:43 2021/11/10
* @Modified By:
*/


/**
-Xms20M 堆初始化大最小容量
-Xmx20M 堆初始化最大容量
-Xmn10M 新生代容量
-XX:SurvivorRatio=8 配置新生代和survivor的大小比例为8:1:1
-XX:+PrintSafepointStatistics 输出safepoint 统计信息,与下面的 PrintSafepointStatisticsCount 相结合使用
-XX:PrintSafepointStatisticsCount=1
-XX:+UnlockDiagnosticVMOptions 这个参数与下面的 GuaranteedSafepointInterval 配合使用,关闭定时让所有线程进入 safepoint
-XX:GuaranteedSafepointInterval=0
-XX:+PrintGCDetails 输出详细的 GC 信息
-XX:+PrintGCApplicationConcurrentTime 两次连续暂停的时间间隔
-XX:+PrintGCApplicationStoppedTime 程序 JVM 暂停的时间


*/
public class SafepointTest {
public static void main(String[] args) {
int size = 1024 * 1024;
byte[] bytes1 = new byte[2 * size];
byte[] bytes2 = new byte[2 * size];
byte[] bytes3 = new byte[3 * size];
byte[] bytes4 = new byte[3 * size];
//当需要分配内存的对象的大小超出了新生代的容量时,对象会被直接分配到老年代
System.out.println("hello world");
}
}

输出结果为:

"D:\\Program Files\\Java\\jdk1.8.0_172\\bin\\java" -Xms20M -Xmx20M -Xmn10M -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 -XX:+UnlockDiagnosticVMOptions -XX:+PrintGCDetails -XX:GuaranteedSafepointInterval=0 -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime "-javaagent:D:\\Program Files\\Java\\IntelliJ IDEA 2017.2\\lib\\idea_rt.jar=61059:D:\\Program Files\\Java\\IntelliJ IDEA 2017.2\\bin" -Dfile.encoding=UTF-8 -classpath "D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\charsets.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\deploy.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\access-bridge-64.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\cldrdata.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\dnsns.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\jaccess.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\jfxrt.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\localedata.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\nashorn.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\sunec.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\sunjce_provider.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\sunmscapi.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\sunpkcs11.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\ext\\zipfs.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\javaws.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\jce.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\jfr.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\jfxswt.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\jsse.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\management-agent.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\plugin.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\resources.jar;D:\\Program Files\\Java\\jdk1.8.0_172\\jre\\lib\\rt.jar;D:\\eclipse-workspace\\jvm\\target\\classes" com.justdodt.jvm.SafepointTest
Application time: 0.0051033 seconds
[GC (Allocation Failure) [PSYoungGen: 6150K->840K(9216K)] 6150K->4944K(19456K), 0.0030366 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
0.132: ParallelGCFailedAllocation [ 11 0 1 ] [ 0 0 0 0 3 ] 0
Total time for which application threads were stopped: 0.0032516 seconds, Stopping threads took: 0.0000747 seconds
hello world
Heap
PSYoungGen total 9216K, used 7307K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 78% used [0x00000000ff600000,0x00000000ffc50e68,0x00000000ffe00000)
from space 1024K, 82% used [0x00000000ffe00000,0x00000000ffed2020,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
Metaspace used 3429K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 373K, capacity 388K, committed 512K, reserved 1048576K
Application time: 0.0010636 seconds
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
0.137: no vm operation [ 10 1 2 ] [ 0 0 0 0 0 ] 0


Polling page always armed
ParallelGCFailedAllocation 1
0 VM operations coalesced during safepoint
Maximum sync time 0 ms
Maximum vm operation time (except for Exit VM operation) 3 ms


Process finished with exit code 0



可以从上面结果看到

Total time for which application threads were stopped: 0.0032516 seconds, Stopping threads took: 0.0000747 seconds


0.0032516
秒表示线程可能已经停止的总时间,这包括上面图中的蓝色和红色部分。
0.0000747
秒表示在上面图中蓝色部分花费的时间,即,在安全点停止所有线程所花费的时间,尽管在 JVM 中此对应的参数为 -XX:+PrintGCApplicationStoppedTime ,她实际上是记录了所有 JVM 暂停,而不仅仅是 GC 暂停。
因此,如果应用程序没有响应
  • 可能是因为 JVM 正试图到达一个安全点,大多数线程已经停止,除了一两个,
  • 或者 JVM 已经到达安全点并正在运行一些内部操作,可能是 GC ,偏向锁撤销,cache line 失效等。


案例2

package com.justdodt.jvm;


import java.util.ArrayList;
import java.util.Collection;


/**
* @Author:JustDoDT
* @Description:
* @Date:Create in 18:33 2021/11/17
* @Modified By:
*/


public class FullGC {


private static final Collection<Object> leak = new ArrayList<>();
private static volatile Object sink;
// Notice that all the stop the world pauses coincide with GC pauses


public static void main(String[] args) {
while(true) {
try {
leak.add(new byte[1024 * 1024]);


sink = new byte[1024 * 1024];
} catch(OutOfMemoryError e) {
leak.clear();
}
}
}
}

在 JVM 启动参数里面加上 -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDetails
,然后会出现如下的结果
Application time: 0.0013760 seconds
Total time for which application threads were stopped: 0.0000805 seconds, Stopping threads took: 0.0000293 seconds
Application time: 0.0268820 seconds
Total time for which application threads were stopped: 0.0059423 seconds, Stopping threads took: 0.0000255 seconds

注意:所有的 Stop The World 暂停都与 GC 暂停一致。


案例3

package com.justdodt.jvm;


/**
* @Author:JustDoDT
* @Description:
* @Date:Create in 18:59 2021/11/17
* @Modified By:
*/


import java.util.concurrent.locks.LockSupport;
import java.util.stream.Stream;


public class BiasedLocks {


private static synchronized void contend() {
LockSupport.parkNanos(100_000);
}

// Biased locks are on by default, but you can disable them by -XX:-UseBiasedLocking
// It is quite possible that in the modern massively parallel world, they should be
// turned back off by default


public static void main(String[] args) throws InterruptedException {


Thread.sleep(5_000); // Because of BiasedLockingStartupDelay


Stream.generate(() -> new Thread(BiasedLocks::contend))
.limit(10)
.forEach(Thread::start);
}


}

当 JVM 参数里面加上 -XX:+PrintGCApplicationStoppedTime 

-XX:+PrintGCDetails
时,输出的结果为

Application time: 0.0013760 seconds
Total time for which application threads were stopped: 0.0000805 seconds, Stopping threads took: 0.0000293 seconds
Application time: 0.0268820 seconds
Total time for which application threads were stopped: 0.0059423 seconds, Stopping threads took: 0.0000255 seconds
 

注意:通过上面输出结果可以看到有很多的 Stop The World,但是没有实际的 GC,这是因为 PrintGCApplicationStoppedTime 实际上显示了所有 STW 暂停。
问题是我们首先需要弄清楚究竟是什么触发了暂停,然后调查暂停的哪一部分需要很长时间,到达安全点的时间,或者执行 VM 操作所花费的时间。因此我们需要在应用程序中加上关于 safepoint 花费时间的参数,即把 -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1
添加到应用程序中。每次发生安全点操作时,添加这两个参数将打印到标准输出或配置的日志文件。从 JDK 8 开始,日志输出由两行组成,如下所示(时间以毫秒为单位):
vmop                    [threads: total initially_running wait_to_block]    [time: spin block sync cleanup vmop] page_trap_count
1.042: no vm operation [ 11 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
4.446: EnableBiasedLocking [ 11 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.532: RevokeBias [ 17 4 2 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.533: RevokeBias [ 18 1 1 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.534: RevokeBias [ 21 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.536: RevokeBias [ 20 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.538: RevokeBias [ 19 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.541: RevokeBias [ 18 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.543: RevokeBias [ 17 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.544: RevokeBias [ 16 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.546: RevokeBias [ 15 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.548: RevokeBias [ 14 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.549: RevokeBias [ 13 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.551: RevokeBias [ 12 0 0 ] [ 0 0 0 0 0 ] 0
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.551: no vm operation [ 10 0 0 ] [ 0 0 0 0 332 ] 0


Polling page always armed
EnableBiasedLocking 1
RevokeBias 12
0 VM operations coalesced during safepoint
Maximum sync time 0 ms
Maximum vm operation time (except for Exit VM operation) 0 ms

由上面的结果可以看出所有的 safepoint 都是由于偏向锁撤销导致的。其中对其中的输出结果做出以下的说明:
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
5.544: RevokeBias [ 16 0 0 ] [ 0 0 0 0 0 ] 0

5.544:自 JVM 启动以来的时间,以秒为单位。
RevokeBias (vmop):在此 safepoint 期间运行的 VM 操作。
16 (total):在请求 safepoint 时 VM 中的线程数量。
0 (initially running):safepoint 操作开始运行的线程数量。
0 (wait to block):VM 开始操作执行时阻塞的线程数。
0 (spin):旋转等待线程到达 safepoint 所花费的时间。
0 (block):等待线程阻塞 safepoint 花费的时间。
0 (sync):safepoint 同步线程所花费的总时间。这是 spin,block,以及其他操作的总时间。这个值就是到达 safepoint 的时间。在 GC 日志中,根据分析,到达 safepoint 的时间
或者线程停止时间
通常仅包括自旋和阻塞时间。
0 (cleanup):花费在内部 VM 清理活动上的时间。
0 (vmop):花费在实际 safepoint 操作 (RevokeBias) 上的时间。这可以是任何 safepoint 操作。
0 (page_trap_count) page_trap 总数。

常见的 SafePoint 调优参数以及讲解


1、建议关闭定时让所有线程进入 Safepoint

对于高并发的应用程序,没有必要定时进入 Safepoint,所以关闭
-XX:+UnlockDiagnosticVMOptions -XX:GuaranteedSafepointInterval=0

2、建议取消偏向锁

在高并发应用中,偏向锁并不能带来性能提升,反而因为偏向锁带来了很多没有必要的某些线程进入 Safepoint 或者 Stop The World。所以,建议关闭偏向锁:-XX:-UseBiasedLocking

3、建议打开循环内添加 Safepoint 参数

防止大循环 JIT 编译导致内部 Safepoint 被优化省略,导致进入 Safepoint 时间边长 -XX:+UseCountedLoopSafepoints

4、建议打开 debug 级别的 safepoint 日志

-Xlog:safepoint=debug:file=safepoint.log:utctime,level,
tags:filecount=50,filesize=100M
注意:上面这个是针对 JDK9以上的版本有效,对于JDK8的不生效

为啥会出现 Safe Region ?


使用 Safepoint 似乎已经完美解决如何进入 GC 的问题了,但是实际情况却并不一定。Safepoint 机制保证了程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint。但是,程序 “不执行” 的时候呢?所谓的程序不执行就是没有分配 CPU 时间,典型的例子就是线程处于 Sleep 状态或者 Blocked 状态,这时候线程响应 JVM 的中断请求,走到安全的地方去挂起,JVM 也显然不太可能等待线程重新被分配 CPU 时间。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码之中,引用关系不会发生变化。在这个区域中任意地方开始 GC 都是安全的。我们也可以把 Safe Region 看作是被扩展了的 Safepoint。
在线程执行到 Safe Region 里面的代码时,首先标识自己已经进入了 Safe Region,那样当这段时间里 JVM 要发起 GC,就不用管标识自己为 Safe Region 状态的线程了。在线程要离开 Safe Region 时,她要检查系统是否已经完成了根节点枚举(或者是整个 GC 过程),如果完成了,那线程就继续执行,否则她就必须等待直到收到可以安全离开 Safe Region 的信号为止。

JVM 何时会 Stop The World


定时进入 SafePoint:每经过 -XX:GuaranteedSafepointInterval 配置的时间,都会让所有线程进入 Safepoint,一旦所有线程都进入,立刻从 Safepoint 恢复。这个定时主要是为了一些没必要立刻 Stop The World 的任务执行,可以设置 -XX:GuaranteedSafepointInterval=0 关闭这个定时,推荐是关闭。
常见 STW 事件如下:
  • [ ]  由于 jstack,jmap,jstat 等命令,也就是 Singal Dispatcher 线程要处理的大部分命令,都会导致 Stop The World,这种命令都需要采集堆栈信息,所以需要所有线程进入 Safepoint 并暂停。
  • [ ]  偏向锁取消(这个不一定会引发整体的 Stop The World,参考 JEP 312:Thread-Local Handshakes):Java 认为,锁大部分情况是没有竞争的(某个同步块大多数都不会出现线程同时竞争锁),所以可以通过偏向锁来提高性能。既在无竞争时,之前获得锁的线程再次获得锁时,会判断是否偏向锁指向我,那么该线程将不用再获得锁,直接就可以进入同步块。但是高并发的情况下,偏向锁会经常失效,导致需要取消偏向锁,取消偏向锁的时候,需要 Stop The World,因为要获取每个线程使用锁的状态以及运行状态。
  • [ ]  Java Instrument 导致的 Agent 加载以及类的重定义:由于涉及到类重定义,需要修改栈上和这个类相关的信息,所以需要 Stop The World。
  • [ ]  Java Code Cache 相关:当发生 JIT 编译优化或者去优化,需要 OSR 或者 Baiilout 或者清理代码缓存的时候,由于需要读取线程执行的方法以及改变线程执行的方法,所以需要 Stop The World
  • [ ]  GC:这个由于需要每个线程的对象使用信息,以及回收一些对象,释放某些堆内存或者直接内存,所以需要 Stop The World。
  • [ ]  JFR 的一些事件:如果开启了 JFR 的 OldObject 采集,这个是定时采集一些存活时间比较久的对象,所以需要 Stop The World。同时,JFR 在 dump 的时候,由于每个线程都有一个 JFR 事件的 buffer,需要将 buffer 中的事件采集出来,所以需要 Stop The World。

参考


https://www.zhihu.com/question/29268019

https://zhuanlan.zhihu.com/p/161710652

https://blanco.io/blog/jvm-safepoint-pauses/#safepoint-operations


文章转载自Just do DT,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论