jvm-Stop the world

Stop the world 介绍

什么是Stop the world?

Java中Stop-The-World机制简称STW,Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互。

等待所有用户线程进入安全点后并阻塞,做一些全局性操作的行为。

在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。不同垃圾收集器的Stop-The-World情况,Serial、Parallel和CMS收集器均存在不同程度的Stop-The-Word情况;而即便是最新的G1收集器也不例外。

Gc为什么要进行stw?

类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。当gc线程在处理垃圾的时候,其它java线程要停止才能彻底清除干净,否则会影响gc线程的处理效率增加gc线程负担,特别是在垃圾标记的时候。

当jvm运行对象存活判定算法的时候,如果当前环境下,对象之间的引用还在发生变化,那么这个算法几乎没法执行,所以常常需要STW来维持秩序。

 

安全点

JVM进行垃圾回收是一个非常复杂的过程,如何进行垃圾标记、什么时候进行垃圾、如果进行垃圾回收等等都非常复杂,当前主流测JVM在垃圾回收时都会进行STW(stop the world),即使宣称非常快的CMS垃圾回收期早期也会STW标记垃圾状态。那么这里有个问题,什么时候进行标记对象是否可以被回收呢?

CPU在执行运算过程时需要把数据从内存中载入到寄存器,运算完后再从寄存器中载入到内存中,Java中对象地址也是这么个过程,设想如果一个Java线程分配一个对象,此时对象的地址还在寄存器中,这时候这个线程失去了CPU 时间片,而此时STW GC发现没有任何GC ROOTS与该对象关联起来,此时这个对象呗认为是垃圾并被回收了,之后CPU重新获得时间片后发现此时对象已经不存在了这时候程序就GG了。

因此不是在任何时候都可以随便GC的,复杂的JVM早就考虑到这个问题,在JVM里面引入了一个叫安全点(Safe Point)的东西来避免这个问题。GC的目的是帮助我们回收不再使用的内存,在多线程环境下这种回收将会变得非常复杂,要安全地回收需要满足一下两个条件:

1.堆内存的变化是受控制的,最好所有的线程全部停止。

2.堆中的对象是已知的,不存在不再使用的对象很难找到或者找不到即堆中的对象状态都是可知的。

为了准确安全地回收内存,JVM是在Safe Point点时才进行回收,所谓Safe Point就是Java线程执行到某个位置这时候JVM能够安全、可控的回收对象,这样就不会导致上所说的回收正在使用的对象。

既然达到Safe Point就可以安全准确的GC,name如何到达Safe Point。

说到这里就要提到如何使线程中断,一般有两种方式:主动式和被动式。主动式JVM设置一个全局变量,线程去按照某种策略检查这个变量一旦发现是Safe Point就主动挂起,被动式就是发个信号,例如关机、Control+C,带来的问题就是不可控,发信号的时候不知道线程处于什么状态。这里HostSop虚拟机采用的是主动式使线程中断。

既然JVM使用的是主动性主动到达安全点,那么应该在什么地方设置全局变量呢?显然不能随意设置全局变量,进入安全点有个默认策略那就是:“避免程序长时间运行而不进入Safe Point”,程序要GC了必须要等线程进入安全点,如果线程长时间不进入安全点这样就比较糟糕了,因此安全点主要咋以下位置设置:

1. 循环的末尾

2. 方法返回前

3. 调用方法的call之后

4. 抛出异常的位置

安全区域

安全点完美的解决了如何进入GC问题,实际情况可能比这个更复杂,但是如果程序长时间不执行,比如线程调用的sleep方法,这时候程序无法响应JVM中断请求这时候线程无法到达安全点,显然JVM也不可能等待程序唤醒,这时候就需要安全区域了。

安全区域是指一段代码片中,引用关系不会发生变化,在这个区域任何地方GC都是安全的,安全区域可以看做是安全点的一个扩展。线程执行到安全区域的代码时,首先标识自己进入了安全区域,这样GC时就不用管进入安全区域的线层了,线层要离开安全区域时就检查JVM是否完成了GC Roots枚举,如果完成就继续执行,如果没有完成就等待直到收到可以安全离开的信号。

stw执行流程:

Stop-The-Word说明:

配置 -XX:+PrintSafepointStatistics –XX:PrintSafepointStatisticsCount=1 参数,虚拟机会打印如下日志文件:

日志分析:

  1. vmop:引发STW的原因,以及触发时间,本例中是GC。该项常见的输出有:<u>RevokeBias、BulkRevokeBias、Deoptimize、G1IncCollectionPause</u>。数字306936.812是虚拟机启动后运行的秒数。GC log可以根据该项内容定位Total time for which application threads…引发的详细信息。

  2. total :STW发生时,JVM存在的线程数目。

  3. initially_running :STW发生时,仍在运行的线程数,这项是Spin阶段的 时间来源

  4. **wait_to_block : **STW需要阻塞的线程数目,这项是block阶段的时间来源

  5. sync = spin + block + 其他。

safepoint的执行分为四个阶段:

  1. Spin阶段。因为jvm在决定进入全局safepoint的时候,有的线程在安全点上,而有的线程不在安全点上,这个阶段是等待未在安全点上的用户线程进入安全点。

  2. Block阶段。即使进入safepoint,用户线程这时候仍然是running状态,保证用户不在继续执行,需要将用户线程阻塞。

  3. Cleanup。这个阶段是JVM做的一些内部的清理工作。

  4. VM Operation. JVM 执行的一些全局性工作,例如 GC, 代码反优化

https://wiki.openjdk.java.net/display/HotSpot/PerformanceTechniques

什么时候会触发Stop-The-Word?

换句话说什么时候会触发进入安全点?

  • Garbage collection pauses(垃圾回收)

  • JIT相关,比如Code deoptimization, Flushing code cache

  • Class redefinition (e.g. javaagent,AOP代码植入的产生的instrumentation)

  • Biased lock revocation 取消偏向锁

  • Various debug operation (e.g. thread dump or deadlock check) dump 线程

我们常见的一些比较直观的场景?

Gc的一些场景如:

1.老年代空间不足。

2.永生代(jdk7)或者元数据空间(jdk8)不足。

3.System.gc()方法调用。

4.CMS GC时出现promotion failed和concurrent mode failure。

5.YoungGC时晋升老年代的内存平均值大于老年代剩余空间。

6.有连续的大对象需要分配。

其他场景:

  • 1.Dump线程--人为因素。

  • 2.死锁检查。

  • 3.堆Dump--人为因素。

 

JIT:

Deoptimization

这个世界是平衡的,有阴就有阳,有优化就有反优化。

为什么优化了之后还要反优化呢?这样对性能不是下降了吗?

通常来说是这样的,但是有些特殊的情况下面,确实是需要进行反优化的。

下面是比较常见的情况:

  1. 需要调试的情况

如果代码正在进行单个步骤的调试,那么之前被编译成为机器码的代码需要反优化回来,从而能够调试。

2.代码废弃的情况

当一个被编译过的方法,因为种种原因不可用了,这个时候就需要将其反优化。

3.优化之前编译的代码

有可能出现之前优化过的代码可能不够完美,需要重新优化的情况,这种情况下同样也需要进行反优化。

代码反优化过程,如果不进行stw,线程继续执行,改变了变量的值,这时候进行反优化的值就有可能是错误的,所以要进行Stop the world;

codeCache是什么?

Java代码在执行时一旦被编译器编译为机器码,下一次执行的时候就会直接执行编译后的代码,也就是说,编译后的代码被缓存了起来。缓存编译后的机器码的内存区域就是codeCache,一块独立于Java堆之外的内存区域。除了JIT编译的代码之外,Java所使用的本地方法代码(JNI)也会存在codeCache中。

 

怎么看codeCatch已满,满了怎么处理?

jvm日志出现如下内容:

Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full. Compiler has been disabled.

此时的后果是:

JIT编译器被停止了,并且不会被重新启动,此时会回归到解释执行;

被编译过的代码仍然以编译方式执行,但是尚未被编译的代码就 只能以解释方式执行了。

 

怎么解决上面的问题?

获取当前codeCache的最大: jinfo -flag ReservedCodeCacheSize pid,通常在64 bit机器上默认是48m。

设置codeCache的空间更大:

-XX:ReservedCodeCacheSize= 更大空间

启用codeCache的的回收机制:

通过在启动参数上增加:-XX:+UseCodeCacheFlushing 来启用;

打开这个选项,在JIT被关闭之前,也就是CodeCache装满之前,会在JIT关闭前做一次清理,删除一些CodeCache的代码;

如果清理后还是没有空间,那么JIT依然会关闭。这个选项默认是关闭的;

 

code cache调优:

以client模式或者是分层编译模式运行的应用,由于需要编译的类更多(C1编译器编译阈值低,更容易达到编译标准),所以更容易耗尽codeCache。当发现codeCache有不够用的迹象时,可以通过启动参数来调整codeCache的大小。

-XX:ReservedCodeCacheSize=256M

那具体应该设置为多大合适呢? 根据监控数据估算,例如单位时间增长量、系统最长连续运行时间等。如果没有相关统计数据,一种推荐的设置思路是设置为当前值(或者默认值)的2倍。

需要注意的是,这个codeCache的值不是越大越好。

对于32位JVM,能够使用的最大内存空间为4g。这个4g的内存空间不仅包括了java堆内存,还包括JVM本身占用的内存、程序中使用的native内存(比如directBuffer)以及codeCache。如果将codeCache设置的过大,即使没有用到那么多,JVM也会为其保留这些内存空间,导致应用本身可以使用的内存减少。

对于64位JVM,由于内存空间足够大,codeCache设置的过大不会对应用产生明显影响。

在JDK 8中,提供了一个启动参数 -XX:+PrintCodeCache 在JVM停止的时候打印出codeCache的使用情况。其中max_used就是在整个运行过程中codeCache的最大使用量。可以通过这个值来设置一个合理的codeCache大小,在保证应用正常运行的情况下减少内存使用。

Flushing code cache的时候,你不知道影响那部分代码的编译,如果不进行stw,代码被判定走jit,但是code cache被清理了,就没法继续编译;如果关闭jit,就是解释执行,效率很低,qps较大的时候,不能满足;

我们一般debug程序的时候,只是关注其中的一部分代码,而且大部分情况下是设置断点,然后单步执行,而JIT的编译单位是class,只要我们执行了class里面的代码,JIT就会对整个class进行编译,而我们实际执行的代码一般都是其中的一部分代码,所以从整个时间效率上来看,采用JIT反而更费时间。也就是说在JVM远程调试这个事情上,禁用JIT(只使用转译器,解释一行执行一条)更合理,所以通过-Djava.compiler=NONE来禁止JIT。

 

Biased lock revocation:

 

https://segmentfault.com/a/1190000017408847

 

http://openjdk.java.net/groups/hotspot/docs/RuntimeOverview.html#Thread%20Management|outline

锁撤销

由于偏向锁失效了,那么接下来就得把该锁撤销,锁撤销的开销花费还是挺大的,其大概的过程如下:

  1. 在一个安全点停止拥有锁的线程。

  2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态。

  3. 唤醒当前线程,将当前锁升级成轻量级锁。所以,如果某些同步代码块大多数情况下都是有两个及以上的线程竞争的话,那么偏向锁就会是一种累赘,对于这种情况,我们可以一开始就把偏向锁这个默认功能给关闭

 

Class redefinition (e.g. javaagent)

 

javaagent: https://segmentfault.com/a/1190000016601560

Java 动态调试技术原理及实践:https://www.ctolib.com/topics-142967.html

 

Gc的时候,哪里用到STW?

目前所有的新生代gc都是需要STW的

Serial:单线程STW,复制算法

ParNew:多线程并行STW,复制算法

Parallel Scavange:多线程并行STW,吞吐量优先,复制算法

G1:多线程并发,可以精确控制STW时间,整理算法

CMS:里面有一个并发标记阶段是不需要stw的,和用户线程一起执行,但是初始标记和重新标记需要stw。

 

 

在新生代进行的GC叫做minor GC,在老年代进行的GC都叫major GC,Full GC同时作用于新生代和老年代。在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,需要stw;

G1收集器运作过程

  

G1 GC Phase

Type

Initial Mark

Stop the world Phase

Root Region Scanning

Concurrent Phase

Concurrent Marking

Concurrent Phase

Cleanup

Stop the world Phase

Remark

 

Stop the world Phase

不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。

(A)、初始标记(Initial Marking)

仅标记一下GC Roots能直接关联到的对象;

且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;

需要"Stop The World",但速度很快;

(B)扫描(Root Region Scanning)、并发标记(Concurrent Marking)

进行GC Roots Tracing的过程;

刚才产生的集合中标记出存活对象;

耗时较长,但应用程序也在运行;

并不能保证可以标记出所有的存活对象;

(C)、最终标记(Final Marking)

为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;

上一阶段对象的变化记录在线程的Remembered Set Log;

这里把Remembered Set Log合并到Remembered Set中;

 

需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;

采用多线程并行执行来提升效率;

(D)、筛选回收(Live Data Counting and Evacuation)

首先排序各个Region的回收价值和成本;

然后根据用户期望的GC停顿时间来制定回收计划;

最后按计划回收一些价值高的Region中垃圾对象;

 

回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;

可以并发进行,降低停顿时间,并增加吞吐量;

G1收集器运行示意图如下:

CMS收集器:

并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器;

  

CMS GC Phase

Type

Initial Mark

Stop the world Phase

Concurrent Mark

Concurrent Phase

Concurrent Preclean

Concurrent Phase

Concurrent Abortable Preclean

Concurrent Phase

Final Remark

Stop the world Phase

Concurrent Sweep

Concurrent Phase

Concurrent Reset

Concurrent Phase

(A)、初始标记(CMS initial mark)

仅标记一下GC Roots能直接关联到的对象;

速度很快;

但需要"Stop The World";

(B)、并发标记(CMS concurrent mark)

进行GC Roots Tracing的过程;

刚才产生的集合中标记出存活对象;

应用程序也在运行;

并不能保证可以标记出所有的存活对象;

(C)、重新标记(CMS remark)

为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;

需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;

采用多线程并行执行来提升效率;

(D)、并发清除(CMS concurrent sweep)

回收所有的垃圾对象;

 

 

GC日志,查看stw耗时时间:

Full GC:

2020-06-12T16:39:37.728-0800: 88.808: [Full GC [PSYoungGen: 116544K->12164K(233024K)[PSOldGen: 684832K->699071K(699072K)] 801376K->711236K(932096K)[PSPermGen: 2379K->2379K(21248K)], 3.4230220 secs] [Times: user=3.40 sys=0.02, real=3.42 secs]

Major GC:

Phase 1: Initial Mark.

2020-06-12T16:23:07.321-0200: 64.425: [GC (CMS Initial Mark) [1 CMS-initial-mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

Phase 2: Concurrent Mark.

2020-06-12T16:23:07.357-0200: 64.460: [CMS-concurrent-mark: 0.035/0.035 secs] [Times: user=0.07 sys=0.00, real=0.03 secs]

Phase 3: Concurrent Preclean.

2020-06-12T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]

Phase 4: Concurrent Abortable Preclean.

2020-06-12T16:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean: 0.167/1.074 secs] [Times: user=0.20 sys=0.00, real=1.07 secs]

Phase 5: Final Mark.

2020-06-12T16:23:08.447-0200: 65.550: [GC (CMS Final Remark) [YG occupancy: 387920 K (613440 K)]65.550: [Rescan (parallel) , 0.0085125 secs]65.559: [weak refs processing, 0.0000243 secs]65.559: [class unloading, 0.0013120 secs]65.560: [scrub symbol table, 0.0008345 secs]65.561: [scrub string table, 0.0001759 secs][1 CMS-remark: 10812086K(11901376K)] 11200006K(12514816K), 0.0110730 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]

Phase 6: Concurrent Sweep.

2020-06-12T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep: 0.027/0.027 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]

Phase 7: Concurrent Reset.

2020-06-12T16:23:08.497-0200: 65.601: [CMS-concurrent-reset: 0.012/0.012 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

Minor GC:

2020-06-12T11:10:34.831+0530:0.208 [ GC(AllocationFailure) 0.208 [ParNew:33280K->4160K(37440K), 0.0627725 secs]33280K->18152K(120768K), 0.0628398 secs] [Times: user=0.18 sys=0.01, real=0.07 secs]

 

  • Real is wall clock time (time from start to finish of the call). This is all elapsed time including time slices used by other processes and time the process spends blocked (for example if it is waiting for I/O to complete).

  • User is the amount of CPU time spent in user-mode code (outside the kernel) within the process. This is only actual CPU time used in executing the process. Other processes, and the time the process spends blocked, do not count towards this figure.

  • Sys is the time spent in OS calls or waiting for system event i,e the amount of CPU time spent inside the kernel within the process (kernel-mode). This means executing CPU time spent in system calls within the kernel, as opposed to library code, which is still running in user-space. Like user, this is only CPU time used by the process.

time nc -z www.java2depth.com

user time 是指程序使用的用户态的时间,一般是不变的。sys time是程序使用的内核态的时间,如果同时运行多个程序,系统会花时间在来回调度和切换进程。运行多个副本时,会出现sys time增加的现象。

 

stw带来的问题,以及相关优化的方式?

1.分析 -XX:+PrintSafepointStatistics –XX:PrintSafepointStatisticsCount=1 产生的日志信息基本上STW的原因都是RevokeBias或者BulkRevokeBias。这个是撤销偏向锁操作,虽然每次暂停的 时间很短,但是特别频繁出现也会很耗时。

一些高并发的系统中,禁掉JVM偏向锁优化,可以提升系统的吞吐量。禁用偏向锁的参数为: -XX:-UseBiasedLocking

 

源码看撤销偏向锁的过程:https://cloud.tencent.com/developer/article/1460325

Hotspot 偏向锁BiasedLocking 源码解析:https://blog.csdn.net/qq_31865983/article/details/105003612

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值