1. STW是什么?
Stop一the一World,简称STW,指的是Gc事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
2. STW出现的时机
- 可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。
- 目前主流的虚拟机采用的都是可达性算法,算法的核心是利用根对象作为起始点,根据对象之间的引用关系,即引用链,通过遍历引用链来判断对象的是否存活。
- 然而,可达性分析算法要求全过程都基于一个能保障一致性的快照中才能够进行分析,简单来说,就是必须全程冻结用户线程的运行。
3. 为什么必须冻结用户线程呢?
- 因为,如果用户线程和垃圾回收线程并发执行,有可能会出现两个问题:浮动垃圾 和 对象消失,详情参考并发的可达性分析。
3.1 停顿的原因
a. 分析工作必须在一个能确保一致性的快照中进行
b. 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
c. 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
d. 停顿的位置:SafePoint (我也不理解)
3.2 什么是 SafePoint
SafePoint 就是一个安全点,可以理解为用户线程执行过程中的一些特殊位置。SafePoint 保存了当前线程的 上下文,当线程执行到这些位置的时候,说明线程当前的状态是 确定的,线程有哪些对象、使用了哪些内存。
因此,只有用户线程处于 SafePoint 的时候,线程才可以安全阻塞。
这意味着,Stop-The-World 需要所有的用户线程处于 SafePoint。
3.3 SafePoint 在哪些位置
SafePoint 插入到代码的某些位置,线程运行到 SafePoint 代码时,将会主动去检查是否需要进入 SafePoint,这个主动检查的过程,被称为 Polling 。
◉ 所有的非计数循环的末尾
(防止循环体的执行时间太长,一直进入不了 SafePoint)
◉ 所有方法返回之前
◉ 每条 Java 编译后的字节码的边界
SafePoint 的数量不能太少,因为这将会导致进入 SafePoint 的前置时间过长,以至于垃圾回收线程等待的时间太长。
SafePoint 的数量也不能太多,过于频繁的 Polling 会有性能损耗。
3.4 如何实现 STW/SafePoint
bash
首先,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 编译好的代码
设置 Poling Page 为不可读,当前用户线程发现该内存页不可读时,就会被阻塞。
Poling Page:
在 JVM 初始化启动的时候,初始化的一个单独的内存页面,这个页面是让运行的编译过的代码的线程进入阻塞状态的关键,是一个全局的 Safepoint Polling Page。
处于阻塞状态
在所有其他用户线程进入 SafePoint 之前,一直阻塞当前用户线程。
处于线程切换状态
一直轮询该用户线程状态,直到线程处于阻塞状态。
3.5 SafePoint日志记录和分析
配置 -XX:+PrintSafepointStatistics --XX:PrintSafepointStatisticsCount=1 参数,
bash
-XX:+PrintSafepointStatistics 打印安全点统计信息,
-XX:PrintSafepointStatisticsCount=n 设置打印安全点统计信息的次数;
3.6 日志分析:
bash
1. vmop: 引发STW的原因,以及触发时间,本例中是GC。该项常见的输出有:RevokeBias、BulkRevokeBias、Deoptimize、G1IncCollectionPause。
数字306936.812是虚拟机启动后运行的秒数。GC log可以根据该项内容定位Total time for which application threads...引发的详细信息。
vmop 输出说明
RevokeBias、BulkRevokeBias、偏向锁取消情况。
Deoptimize、
G1IncCollectionPause GC GC 执行情况。
2. total : STW发生时,JVM存在的线程数目。
3. initially_running : STW发生时,仍在运行的线程数,这项是Spin阶段的 时间来源
4. wait_to_block : STW需要阻塞的线程数目,这项是block阶段的时间来源
5. sync = spin + block + 其他。
3.7 由日志可以看出safepoint的执行分为四个阶段:
bash
1. Spin阶段。因为jvm在决定进入全局safepoint的时候,有的线程在安全点上,而有的线程不在安全点上,这个阶段是等待未在安全点上的用户线程进入安全点。
2. Block阶段。即使进入safepoint,用户线程这时候仍然是running状态,保证用户不在继续执行,需要将用户线程阻塞。
3. Cleanup。这个阶段是JVM做的一些内部的清理工作。
4. VM Operation. JVM 执行的一些全局性工作,例如 GC, 代码反优化。
3.8 优化说明
bash
分析 -XX:+PrintSafepointStatistics --XX:PrintSafepointStatisticsCount=1
产生的日志信息基本上STW的原因都是RevokeBias或者BulkRevokeBias。
这个是撤销偏向锁操作,虽然每次暂停的 时间很短,但是特别频繁出现也会很耗时。
一些高并发的系统中,禁掉JVM偏向锁优化,可以提升系统的吞吐量。
禁用偏向锁的参数为: -XX:-UseBiasedLocking
3.9 引起长时间STW原因
bash
- GC
- RevokeBias 撤销偏向锁操作也会消耗很长时间。在高并发系统中,建议禁用偏向锁。
- 撤销偏向锁,增加 -XX:-UseBiasedLocking 虚拟机参数
4. 示例代码
- 被STW中断的应用程序线程会在完成GC之后恢复,频繁的中断会让用户感觉像是网速不快造成的电影卡顿一样,所以我们要减少STW的发生
- STW事件和采用哪款GC无关,所有的GC都有这个事件。
- 哪怕是G1也不能完全避免STW情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
- STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
- 开发中采用System.gc();会导致STW的发生。
java
package com.zishi.jvm;
import java.util.ArrayList;
import java.util.List;
public class StopTheWorldDemo {
public static class WorkThread extends Thread {
List<byte[]> list = new ArrayList<byte[]>();
public void run() {
try {
while (true) {
for(int i = 0;i < 1000;i++){
byte[] buffer = new byte[1024];
list.add(buffer);
}
if(list.size() > 10000){
list.clear();
System.gc();//会触发full gc,进而会出现STW事件
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static class PrintThread extends Thread {
public final long startTime = System.currentTimeMillis();
public void run() {
try {
while (true) {
// 每秒打印时间信息
long t = System.currentTimeMillis() - startTime;
System.out.println(t / 1000 + "." + t % 1000);
Thread.sleep(1000);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void main(String[] args) {
WorkThread w = new WorkThread();
PrintThread p = new PrintThread();
w.start();
p.start();
}
}
W线程当中的GC触发了STW,进而干扰了P线程有规律性打印。打印变得杂乱无章
打印输出:
bash
0.0
1.100
2.103
3.112
4.122
5.134
6.143