关于jvm:深入浅出解析JVM中的Safepoint-|-得物技术

6次阅读

共计 7786 个字符,预计需要花费 20 分钟才能阅读完成。

1. 初识 Safepoint-GC 中的 Safepoint

最早接触 JVM 中的平安点概念是在读《深刻了解 Java 虚拟机》那本书垃圾回收器章节的内容时。置信大部分人也一样,都是通过这样的形式第一次对平安点有了初步意识。无妨,先温习一下《深刻了解 Java 虚拟机》书中平安点那一章节的内容。

书中是在解说垃圾收集器 - 垃圾收集算法的章节引入平安点的介绍,为了疾速精确地实现 GC Roots 枚举,防止为每条指令都生成对应的 OopMap 造成大量存储空间的节约,只在“特定的地位”生成对应的 OopMap,这些地位被称为平安点。而后,书中提到了平安点地位的抉择规范是:是否能让程序长时间执行;所以会在办法调用、循环跳转、异样跳转等处才会产生平安点。

书中还提到了 JVM 如何在 GC 时让用户线程在最近的平安点处停顿下来:领先式中断和主动式中断。领先式中断不须要线程的执行代码被动去配合,在 GC 产生时,零碎首先把所有用户线程全副中断,如果发现有用户线程中断的中央不在平安点上,就复原这条线程执行,让它一会再从新中断,直到跑到平安点上。而主动式中断的思维是当 GC 须要中断线程时,不间接对线程操作,仅仅简略地设置一个标记位,各个线程执行过程时不停地被动去轮询这个标记,一旦发现中断标记为真就本人在最近的平安点上被动中断挂起。当初基本上所有虚拟机实现都采纳主动式中断形式来暂停线程响应 GC 事件。

总结一下初识平安点学到的知识点:

  • JVM GC 时须要让用户线程在平安点处停顿下来(Stop The World)
  • JVM 会在办法调用、循环跳转、异样跳转等处搁置平安点
  • JVM 通过被动中断形式达到全局 STW:设置一个标记位,各个线程执行过程时不停地被动去轮询这个标记,一旦发现中断标记为真就本人在最近的平安点上被动中断挂起。

以上基本上就是《深刻了解 Java 虚拟机》这本书对 JVM 平安点的所有介绍了,过后感觉平安点还是很好了解,认为平安点就是在垃圾回收时为了 STW 而设计的。

起初发现,通过一些线上问题和网上看到无关平安点乏味的示例,发现平安点其实也不简略,不只有 GC 才会用到平安点;简略的代码如果写的不当,平安点也会带来一些莫名其妙的问题;其在 JVM 外部的实现以及 JIT 对它的优化,也常常让人摸不着头脑。本文尝试在初识平安点后已知知识点的根底上,通过一段简略的示例代码,多问几个为什么,来进一步更全面的理解一下平安点。

2. 通过一段示例代码深刻分析 Safepoint

2.1  示例代码

这段示例代码可间接复制到本地运行,本文所有对示例代码的运行环境都是 jdk 1.8。


public static AtomicInteger *counter* = new AtomicInteger(0);

public static void main(String[] args) throws Exception{long startTime = System.*currentTimeMillis*();

Runnable runnable = () -> {System.*out*.println(*interval*(startTime) + "ms 后," + Thread.*currentThread*().getName() + "子线程开始运行");

for(int i = 0; i < 100000000; i++) {*counter*.getAndAdd(1);

}

System.*out*.println(*interval*(startTime) + "ms 后," + Thread.*currentThread*().getName() + "子线程完结运行, counter=" + *counter*);

};

Thread t1 = new Thread(runnable, "zz-t1");

Thread t2 = new Thread(runnable, "zz-t2");

t1.start();

t2.start();

System.*out*.println(*interval*(startTime) + "ms 后,主线程开始 sleep.");

Thread.*sleep*(1000L);

System.*out*.println(*interval*(startTime) + "ms 后,主线程完结 sleep.");

System.*out*.println(*interval*(startTime) + "ms 后,主线程完结,counter:" + *counter*);

}

private static long interval(Long startTime) {return System.*currentTimeMillis*() - startTime;

}

}


示例代码中主线程启动两个子线程,而后主线程睡眠 1s,通过打印工夫来察看主线程和子线程的执行状况。按情理来说这里主线程和两个子线程独立并发,没有任何显性的依赖,主线程的执行是不会受子线程影响的:主线程睡眠完结后会间接完结。然而执行后果却和冀望不一样。

执行后果如下方动图展现:

从执行后果看,主线程在启动两个线程后进入睡眠状态,代码中指定睡眠工夫为 1s,然而主线程却在 3s 多之后才睡眠完结。是什么导致了主线程睡过头了呢,从后果来看主线程睡觉完结工夫和子线程完结工夫是统一的。所以,咱们有理由狐疑主线程没有按时提前结束应该是被两个子线程阻塞了。

2.2  先给论断

因为 VMThread 的某些操作须要 STW,主线程在 sleep 完结前进入了 JVM 全局平安点,而后主线程要期待其余线程全副进入平安点,所以主线程被长时间没有进入平安点的其余线程给阻塞了。

2.3  验证论断

增加 JVM 打印平安点日志参数 -XX:+PrintSafepointStatistics 后再执行下面的实例代码,后果如下截图:

能够从平安点日志中看到,JVM 想要执行no vm operation,这个操作须要线程进入平安点,整个期间有 12 个线程,正在运行的线程有两个,须要期待这两个线程进入平安点,期待耗时 2251ms。

加上 -XX:+SafepointTimeout 和-XX:SafepointTimeoutDelay=2000 参数后执行代码能够进一步看期待哪两个线程进入平安点。

果然和猜想的一样,没有达到平安点的两个线程正是示例代码中定义的 zz-t1 和 zz-t2 线程。

2.4  为什么

到这里这个示例的执行后果的起因曾经有了论断并且失去了验证,基本上曾经知其然了。然而如果深刻思考一下,初识平安点时学到的知识点还不能解释,所以为了知其所以然,这里提了几个为什么。

(1)为什么会进入平安点

换句话问,是什么触发了进入平安点?

由初识平安点失去的基础知识晓得进入平安点须要两个条件:

  • JVM 操作设置了被动中断标记
  • 运行的代码中存在平安点

首先想到的是 GC 触发 JVM 设置被动中断标记,加上 -XX:-PrintGC再执行示例代码并没有打印 GC 日志,能够排除掉 GC。

既然不是 GC,还是再回到平安点日志上寻找线索吧,发现有个 vmop(虚拟机操作类型):no vm operation 对于no vm operation,网上有大神通过解析 JVM 源码失去了论断,这里不对 JVM 源码开展做具体解读,间接给论断:

在 JVM 失常运行的时候,如果设置了进入平安点的距离,就会隔一段时间判断是否有代码缓存要清理,如果有,会进入平安点。这个触发条件不是 VM 操作,所以会将 \_vmop\_type 设置成 -1,输入日志的时候打印对应的 「no vm operation」,也就是咱们看到的平安点日志。

在 VM 操作为空的状况下,只有满足以下 3 个条件,也是会进入平安点的:

1、VMThread 处于失常运行状态

2、设置了进入平安点的间隔时间

3、SafepointALot 是否为 true 或者是否须要清理

用 Java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal 2>&1 | grep Safepoint 命令查看 JVM 对于平安点的默认参数:

发现 GuaranteedSafepointInterval 默认设置成了 1 秒,每隔 1s 就会尝试进入平安点。

那么,批改 GuaranteedSafepointInterval 参数值,看看是否能阻止进入平安点。

GuaranteedSafepointInterval参数是 JVM 诊断参数,批改这个参数的值,须要配合 -XX:+UnlockDiagnosticVMOptions一起应用。

另外不倡议在线上对这个参数的值做批改。

  • 敞开定时进入平安点

通过 -XX:GuaranteedSafepointInterval = 0 敞开定时进入平安点,看看代码运行后果是怎么样的

由运行后果能够看出,敞开定时进入平安点后,主线程睡眠 1s 后失常完结,不受其余线程阻塞。从平安点日志看,之前期待进入平安点的两个线程也没有了。

  • 调大定时进入平安点间隔时间

由打印的执行后果能够看到子线程运行工夫是 3s 多,如果把进入平安点间隔时间调整为 5s,即在子线程完结之后再尝试进入平安点是不是也能防止期待子线程进入平安点呢?批改参数 -XX:GuaranteedSafepointInterval = 5000 调整平安点间隔时间再次执行后果:

从执行后果能够看出,调大平安点间隔时间和敞开定时进入平安点的成果是一样的,也能够防止期待子线程进入平安点的。

(2)主线程是在哪里进入的平安点

从示例代码在默认 JVM 参数执行后果看,主线程睡眠工夫超过了 3s,事实上主线程是在 Thread.sleep() 办法外部进入平安点。 这里对 JVM 平安点实现的源码简略做一下剖析:

Safepoint 实现源代码:Safepoint.cpp

读源码太吃力,看正文吧,所幸从正文中也能找到答案。下面截图的正文说在程序进入 Safepoint 的时候,Java 线程可能正处于的五种不同的状态,针对不同的状态的不同解决机制。假如当初有一个操作触发了某个 VM 线程所有线程须要进入 SafePoint,如果其余线程当初:

  • 运行字节码 :运行字节码时,解释器会看线程是否被标记为 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 住)。

再看一下 Thread.sleep 办法的申明,就和下面 Safepoint.cpp 源码正文截图红框对上了,Thread.sleep正是一个 native 办法。

Thread.sleep(0)在 RocketMQ 中的妙用

下面这段代码是 RocketMQ 的一段代码,16 年最早版本的实现 for 循环内每循环 1000 次会调用一次 Thread.sleep(0),这貌似是一段无用的代码,作者实在的目标是为了在这里搁置一个平安点,防止 for 循环运行工夫过长导致系统长时间 SWT。从代码的变更记录看,22 年 9 月份有人对这段代码换了一种写法:把 for 循环变量类型定义成 long 型,同时正文掉了循环外部Thread.sleep(0) 代码,为什么能够这样写以及为什么要这样写这里先按下不表。

(3)子线程为什么无奈进入平安点

当初曾经晓得了主线程为什么进入会进入平安点,以及主线程在哪里进入的平安点,依照已知知识点 JVM 会在循环跳转处和办法调用处搁置平安点,为什么子线程没有进入平安点?

可数循环和不可数循环

JVM 为了防止平安点过多带来过重的累赘,对循环有一项优化措施,认为循环次数较少的话,执行工夫应该不会太长,所以应用 int 类型和范畴更小的数据类型作为索引值的循环默认是不会被搁置平安点的。这种循环被称为可数循环,绝对应的,应用 long 或者范畴更大的数据类型作为索引值的循环就被称为不可数循环,将被搁置平安点。

在示例代码中,子线程的循环索引值数据类型是 int,也就是可数循环,所以 JVM 没有在循环跳转处搁置平安点。

把循环索引值数据类型改成 long 型,循环成为不可数循环,就可能胜利在循环跳转处搁置平安点,防止子线程长时间无奈进入平安点阻塞主线程。

从下面的执行后果能够看到,把循环索引值数据类型改成 long 型,主线程在睡眠 1s 之后立刻完结了睡眠,并没有期待子线程的执行。

到这里,也就晓得为什么下面贴的 RocketMQ 大那段代码,把循环索引值数据类型改成 long 型能够替换循环外部 Thread.Sleep(0) 达到搁置平安点的目标了。

其实,还能够通过 -XX:+UseCountedLoopSafepoints 参数敞开 JVM 对可数循环搁置平安点的优化。上面的执行后果能够看出,增加了 -XX:+UseCountedLoopSafepoints 参数后,也能让运行后果达到预期。

还有一个纳闷

认真看实例代码,发现子线程循环体内调用了 AtomicInteger 类的 getAndAdd 办法,再深刻看 jdk getAndAdd办法的实现,发现底层是调用了 sun.misc.Unsafe#getIntVolatile 这个办法和Thread.sleep 办法一样,也是一个 native 办法,为什么这里没有进入像 Thread.sleep 办法一样进入平安点?

是的,好可怕,的确被优化了,被 JIT 给优化了。为了验证是被 JIT 优化了,能够用

-Djava.compiler=NONE敞开 JIT 而后看一下运行后果。

从运行后果看,敞开了 JIT 优化后,主线程的确在睡眠 1s 后立刻完结了,不过子线程运行的工夫比 JIT 优化开启时多了不少。所以,JIT 还是可能带来肯定的性能优化的,有时也会带来一些奇怪的景象。

3. 更全面的平安点定义

区别于初识平安点的时候局限于 GC 中的平安点概念,这里给平安点一个比拟全面的定义:

Safepoint 能够了解成是在代码执行过程中的一些非凡地位,当线程执行到这些地位的时候,线程能够暂停。在 SafePoint 保留了其余地位没有的一些以后线程的运行信息,供其余线程读取。这些信息包含:线程上下文的任何信息,例如对象或者非对象的外部指针等等。咱们个别这么了解 SafePoint,就是线程只有运行到了 SafePoint 的地位,他的所有状态信息,才是确定的,也只有这个时候,才晓得这个线程用了哪些内存,没有用哪些;并且,只有线程处于 SafePoint 地位,这时候对 JVM 的堆栈信息进行批改,例如回收某一部分不必的内存,线程才会感知到,之后持续运行,每个线程都有一份本人的内存应用快照,这时候其余线程对于内存应用的批改,线程就不晓得了,只有再进行到 SafePoint 的时候,才会感知。

4. 什么时候会进入 Safepoint

当 VM Thread 须要做 vm  操作时会让线程进入平安点,vm 操作类型有很多,能够参考 VM_OP_ENUM 源码 vmOperations.hpp。上面是几种常常产生的进入 Safepoint 的情景:

(1)GC:因为须要每个线程的对象应用信息,以及回收一些对象,开释某些堆内存或者间接内存,所以须要 进入 Safepoint 来 Stop the world;

(2)定时进入 SafePoint:每通过 -XX:GuaranteedSafepointInterval 配置的工夫,都会让所有线程进入 Safepoint,一旦所有线程都进入,立即从 Safepoint 复原。这个定时次要是为了一些没必要立即 Stop the world 的工作执行,能够设置-XX:GuaranteedSafepointInterval=0 敞开这个定时。

(3)因为 jstack,jmap 和 jstat 等命令,会导致 Stop the world:这种命令都须要采集堆栈信息,所以须要所有线程进入 Safepoint 并暂停。

(4)偏差锁勾销:锁大部分状况是没有竞争的(某个同步块大多数状况都不会呈现多线程同时竞争锁),所以能够通过偏差来进步性能。即在无竞争时,之前取得锁的线程再次取得锁时,会判断是否偏差锁指向我,那么该线程将不必再次取得锁,间接就能够进入同步块。然而高并发的状况下,偏差锁会常常生效,导致须要勾销偏差锁,勾销偏差锁的时候,须要 Stop the world,因为要获取每个线程应用锁的状态以及运行状态。

(5)Java Instrument 导致的 Agent 加载以及类的重定义:因为波及到类重定义,须要批改栈上和这个类相干的信息,所以须要 Stop the world

(6)Java Code Cache 相干:当产生 JIT 编译优化或者去优化,须要 OSR 或者 Bailout 或者清理代码缓存的时候,因为须要读取线程执行的办法以及扭转线程执行的办法,所以须要 Stop the world

5. 防止 Safepoint 副作用

Safepoint 在肯定水平上是能够了解成是为了让所有用户线程进展(Stop The World)而设计的。STW 对利用零碎来说是一件很可怕的事件,JVM 不论是在 GC 还是在其余的 VM 操作上都在致力防止 STW 和缩小 STW 工夫。

平安点最次要的副作用就是可能导致 STW 工夫过长,应该竭力防止这点副作用。

对第一个进入平安点的线程来说,STW 是从它进入平安点开始的,如果有某个线程始终无奈进入平安点就会导致进入平安点的工夫始终处于期待状态,进而导致 STW 的工夫过长。所以,应防止线程执行过长无奈进入平安点的状况。

可数循环体内执行工夫过长以及 JIT 优化导致无奈进入平安点的问题是最常见的无奈进入平安点的状况。在写大循环的时候能够把循环索引值数据类型定义成 long。

在高并发利用中,偏差锁并不能带来性能晋升,反而因为偏差锁勾销带来了很多没必要的某些线程进入平安点。所以倡议敞开:-XX:-UseBiasedLocking

jstack,jmap 和 jstat 等命令,也会导致进入平安点。所以,生产环境应该敞开 Thead dump 的开关,防止 dump 工夫过长导致利用 STW 工夫过长。

参考文献:

[1]《深刻了解 java 虚拟机》

[2]http://psy-lob-saw.blogspot.com/2015/12/safepoints.html

[3]https://xie.infoq.cn/article/a80542aca7ad53efaaab1a27a

[4]https://zhuanlan.zhihu.com/p/161710652

文:Simon

更多精彩文章请拜访得物技术官网:tech.dewu.com

流动举荐:得物技术沙龙开始报名啦!点击关注得物技术沙龙理解详情!

正文完
 0