Java安全点safepoint
安全点是指程序的某些特定的位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停。
当程序执行某些特定的操作时需要避免其他线程的操作导致程序的状态不一致或不确定,因此此时会让应用程序产生停顿,这个停顿称为STW(Stop The World)。HotSpotVM使用安全点来实现STW,比如发生GC时,需要暂停所有活动线程,但是线程在这个时刻还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待GC结束。
进入安全点时机
JVM中的许多操作需要保证线程的状态是一致的,不能再某些关键操作进行时让线程随意运行,否则可能导致数据不一致或系统崩溃。因此JVM会要求所有线程进入安全点,下面是几种经常发生的进入Safepoint的情形:
- 垃圾回收:GC根节点枚举
- 代码脱优化(Deoptimization):JIT编译生成的本地代码回退到解释模式
- 堆栈遍历:如执行jmap或jstack,需要采集堆栈信息
- 偏向锁撤销:需要获取所有线程使用锁的状态以及运行状态
- Java Insturemnt导致的Agent加载以及类的重定义
以上这些操作执行过程中所有的线程都必须停下来,否则会导致操作结果的准确性无法保证,因此需要所有的线程约定在某一些特定的位置停下来,这个位置就是安全点。
此外程序也会根据-XX:GuaranteedSafepointInterval 配置的时间,定时让所有线程进入 Safepoint,一旦所有线程都进入,立刻从 Safepoint 恢复。这个定时主要是为了一些没必要立刻STW的任务执行
安全点暂停
让所有的线程暂停通常有两种方式:
- 抢占式中断:系统把所有的用户线程全部中断,对于没有到达安全点的线程则恢复执行直到跑到安全点
- 主动式中断:不直接操作线程,而是设置一个标志位。线程执行过程中主动地去检查这个标志位即可,发现需要中断则到最近的安全点上主动挂起
主流的虚拟机都是通过主动式中断的方式,而程序需要进入安全点时Java线程可能存在几种不同的状态:
- 处于解释执行字节码的状态:解释器只需要在获取下一条字节码的时候主动检查安全的状态即可
- 处于执行native代码的状态:VMThread不会等待线程进入安全点,而是执行JNI退出后让线程主动检查安全点状态
- 处于编译代码执行状态:编译的时候会在合适的位置插入读取polling page内存页的指令。HotSpot通过内存保护陷阱的方式实现,把轮询操作精简至只有一条汇编指令的程度(test %eax, 0x160100)。当需要暂停用户线程时,虚拟机会把0x160100的内存页(polling page)设置为不可读。当线程执行到test指令时会因为读取了不可读的内存页而产生一个自陷异常信号,在预先注册的异常处理器中会处理这个信号,挂起线程实现等待
- 处于阻塞状态:例如线程在等待锁或者睡眠,那么线程的阻塞状态将不会结束,直到安全点标志被清除
- 处于线程切换状态:切换前会检测安全点状态,如果呀求进入安全点则不允许切换
安全区域:
安全区域可以确保在某一段代码片段中引用关系不会发生变化,因此在这个区域进行操作是安全的,可以认为是被扩展拉伸了的安全点。常见的如用户线程处于Sleep状态或者Blocked状态。当用户线程执行到安全区域里面的代码时会表示自己已经进入了安全区域,虚拟机如果需要进入安全点时不需要等待这些已经声明在安全区域的线程,只需要这些线程退出安全区域时判断是否需要进入安全点即可。
安全点位置
安全点的位置会生成polling代码询问VM是否要暂停,polling操作是会有一定的开销的,因此安全点位置的选择不能太多,基本是以让程序长时间执行为标准选定的。产生安全点的位置有:
- 方法调用
- 循环跳转
- 异常跳转
- 对象分配
- 锁操作
由于polling操作是有开销的,因此JIT编译器会在可能的情况下尝试消除它。其中一个优化就是删除counted loops中的安全点轮询(使用long进行遍历时会视为uncounted loops)。然而当循环的执行时间比较长,那么就可能会导致该线程长时间无法进入安全点,导致应用有一个比较长时间的停顿。
自JDK10以来,HotSpot实现了Loop Strip Mining优化,解决了在counted loop中安全点轮询问题,且没有太大的开销。
相关虚拟机参数
- -XX:+PrintSafepointStatistics:打印安全点日志参数。包括从开始到进入安全点需要的时间,触发安全点的原因,安全点内部花费的时间
- -XX:PrintSafepointStatisticsCount:此参数用来指定在打印多少次安全点统计数据后自动关闭该功能。如果你设置为0,则表示无限次打印,直到JVM退出;如果设置为一个正整数N,则会在记录N次安全点统计数据之后停止记录
- -XX:GuaranteedSafepointInterval:进入安全点的时间间隔,默认为1s,即每个1s就会尝试进入安全点
- GuaranteedSafepointInterval参数是JVM诊断参数,修改这个参数的值,需要配合-XX:+UnlockDiagnosticVMOptions一起使用
- -XX:+UseCountedLoopSafepoints:关闭对可数循环安全点轮询的优化
- -XX:+SafepointTimeout:启用安全点超时检测机制
- -XX:SafepointTimeoutDelay:设置超时警告的阈值(默认为10000ms)
一般来说避免在生产环境开启安全点相关的日志,一方面会有额外的性能开销,同时也可能导致日志量爆炸。一般可以通过JFR来跟踪JVM中安全点相关的事件,可以看到详细的安全点活动记录,或者通过一些GC以及线程的监控来辅助排查。
避免安全点的副作用
安全点最大的副作用是会导致STW的时间过长,因此可以从两个角度优化:降低进入安全点的时间以及减少STW操作。常见的优化方式如下:
- 可数循环体如果耗时过长可以将循环索引值的类型定义为long
- 关闭偏向锁
- 减少jstack、jmap、jstat等命令的使用
参阅
- JVM源码分析之安全点safepoint
- 真是绝了!这段被JVM动了手脚的代码!
- 《深入理解Java虚拟机-JVM高级特性与最佳实践(第三版)》
- 《深入剖析Java虚拟机-源码剖析与实例详解(基础卷)》