JVM系列之STW、并行与并发、安全点与安全区域

扫描下方二维码或者微信搜索公众号菜鸟飞呀飞,即可关注微信公众号,阅读更多Spring源码分析Java并发编程Netty源码系列MySQL工作原理文章。

微信公众号
微信公众号

前言

在平时实际工作中,其实接触 JVM 的机会比较少,而学习 JVM 时,大部分时候也是看书或者网上看博客教程,然而在学习过程中,经常会碰到很多陌生的名词,难以理解,尤其是在垃圾回收器中,因此本文找了几个和垃圾回收器相关知识中联系比较紧密的名词解释一下。

Stop The World

Stop The World 简称 STW,它指的是程序在运行过程中,在发生 GC 的过程中,会让所有的用户线程都暂停下来,此时对应用程序而言,相当于整个程序都停止运行了,因此称之为 Stop The World。STW 这个过程是 JVM 在后台自动发起和自动完成的,在用户不可见的情况下,将所有应用程序停顿下来。

尽管目前存在很多垃圾回收器,并且新的垃圾回收器的效率越来越高,但是目前为止,所有的垃圾回收器都无法避免 STW,即使是号称低延时的 CMSG1 垃圾回收器,也存在 STW。这是因为在标记阶段,使用可达性分析算法进行分析时,整个应用程序的数据都应该处于一致性视图当中,这是为了保证可达性分析算法的准确性,因此需要暂停所有的用户线程。

显然 STW 对应用程序十分不友好,STW 的时间越长,应用程序卡顿的时间就越长,我们应该尽量减少 STW 的次数,以及每次 STW 的时间

程序中的并行与并发

通常情况下,在操作系统中,我们所说的并行指的是在多个 CPU 上(或者说多核),多个线程或者进程同时运行,这些线程相互之间因为运行在不同的 CPU 上,因此不会产生竞争资源的现象。如下图所示,A、B、C 为三个进程,同一时刻,它们是真正意义上的同时运行

并行
并行

并发指的是在同一个 CPU 上,严格来说是在同一个处理器上,一段时间内,同时运行着多个线程或者进程。众所周知,CPU 执行任务是分时间片来执行的,在一个时间片内只会有一个线程运行,因此并发不是真正意义上的同时运行。而因为每一个时间片特别小,小到给人们的感觉是这几个线程都没有间断过,而是多个线程在同时运行。

如下如所示,A、B、C 为三个线程,在一个时间片内,只会有一个线程在执行。所以说,并发指的是在一段时间内,多个线程在同一个 CPU 上运行,这里需要强调地是一段时间内

并发
并发

在并发情况下,因为是多个线程运行在同一个 CPU 上,因此它们相互之间是竞争关系,会争夺 CPU 资源。

垃圾回收器中的并行与并发

与程序中的并行、并发不一样的是,垃圾回收器中的并行(Parallel)指的是在同一时刻,有多个垃圾回收线程在多个 CPU 上同时执行,在垃圾回收线程执行期间,用户线程会全部暂停,因此会产生 STW。在 7 中经典的垃圾回收器中,ParNew、Parallel Scavenge、Parallel Old 这几个垃圾回收器就是属于并行的垃圾回收器。

与并行相对的就是串行(Serial),在单核的 CPU 上,只有一个垃圾回收线程执行,且在垃圾回收线程执行期间,用户线程也会暂停,因此也会产生 STW。Serial、Serail Old 这两个垃圾回收器就是属于串行的垃圾回收器。垃圾回收器中的并行与串行的示意图如下。

并行垃圾回收器
并行垃圾回收器

垃圾回收器中的并发指的是垃圾回收线程和用户线程同时执行(这里的同时,指的也是在一个时间段内),在多个 CPU 上场景下,用户线程运行在一个 CPU 上,垃圾回收线程运行在另一个 CPU 上,此时垃圾回收线程和用户线程是处于并行状态。并发的垃圾回收器的示意图如下。

并发垃圾回收器
并发垃圾回收器

并发的垃圾回收器在进行垃圾回收过程中,从开始到结束,垃圾回收线程和用户线程并不是全程都是并行的,也会出现交替执行,因此也会出现 STW 的现象。为什么会这样呢?这是由底层的垃圾回收算法导致的,因为在标记阶段,为了保证标记的正确性,必须要停止用户线程。

在 7 种经典的垃圾回收器中,并发的垃圾回收器的代表有 CMS、G1

安全点

在程序执行的过程中,并不是在任何地方都能进行 GC,而是必须得在安全点(Safepoint) 处才能进行。

安全点的选择十分重要,如果程序在执行过程中,安全点过多,GC 可能过于频繁,那么就可能导致程序运行时的性能下降,而如果安全点过少,那么就会导致系统需要间隔好长时间才能执行一次 GC,这样每次 GC 需要回收的垃圾过多,导致挺多的时间会很长。

那么在程序中,什么样的地方才能被称之为安全点呢?实际上,选取安全点的标准是是否具有让程序长时间执行的特征,因为大部分指令的运行时间都很短,我们可以接受让这些指令执行完以后再进行垃圾回收,而如果某个指令执行时间很长,程序如果让这些指令执行完再进行垃圾回收,这样就可能导致垃圾回收进行得不够及时。

因此通常会将如方法调用、循环跳转、异常跳转等指令序列复用作为安全点。

现在我们知道了程序在遇到安全点后会停下来进行 GC,但问题来了,一个应用程序中,同一时刻可能有很多个线程在执行,但是所有线程在同一时刻都遇到安全点的概率太小了。通常情况下,当要发生 GC 时,可能是其中一部分线程遇到了安全点,而另一部分线程还没到安全点,那这个时候该怎么办呢?答案是让其他线程也都跑到最近的安全点停下来。

而让其他线程都跑到最近点的安全点停下来有两种做法:

  1. 抢占式中断,也就是中断所有的线程,然后再判断每个线程是否在安全点,如果不在,就恢复线程,让线程继续运行,直到跑到最近的安全点,这种方式目前已经没有虚拟机在采用了。
  2. 主动式中断,当要发生 GC 时,设置一个标志位,然后其他线程运行到安全点时,先判断这个标志位,如果标志位的值表示此时需要进行 GC,那么线程就停下来,将自己中断挂起,否则继续运行。

安全区域

安全点的存在使得程序在运行时,在不太长的时间内,就会遇到可以进入 GC 的安全点,但是如果遇到线程”不执行“的情况该怎么办呢?例如线程在 Sleep 或者 Blocked,我们不可能让其他线程都等在那儿,然后等待这个线程从 Sleep 或者 Blocked 中醒来,再运行到安全点,因为你无法确定这个线程会 Sleep 或者 Blocked 多长时间。

为了解决这种情况,安全区域(Safe Region) 这个概念就出现了,安全区域是指:在一个代码片段内,对象之间的引用关系不会发生变化,那么在这个区域中的任何位置进行垃圾回收都是没问题的。

在实际执行过程中,当线程进入到安全区域时,会先将自己标识为进入 Safe Region 状态,那么当需要进行 GC 时,JVM 会忽略已经进入到 Safe Region 的线程;当线程要离开安全区域时,会判断 GC 是否完成,如果 GC 已经完成,则可以离开 Safe Region,否则继续等待,直到接收到 GC 完成的信号。

总结

本文主要介绍了在操作系统中和在垃圾回收器中,对并行与并发这两个概念的理解,这对后面即将介绍的 7 款垃圾回收器会有很大帮助。另外还介绍了安全点和安全区域两个概念,它们的存在是为了告诉线程,当需要发生 GC 时,它们可以在哪些地方停顿下来。

参考

  • 周志明《深入理解 Java 虚拟机》第三版
微信公众号
微信公众号

猜你喜欢

转载自juejin.im/post/5eea4ade6fb9a058805983ab