这两天再捣鼓一个JVM长时间STW的问题,看了不少文章,怕过两天就忘掉了,还是写点东西记录下来,虽然到目前这一刻还是没找到root cause,但是期间却学到不少东西,记下来也是很有价值的。
话不多说,先上问题
在线上的JVM我们发现最近一些比较奇怪的毛刺,在此之前,我们遇到一些GC 的毛刺,是因为没有硬性配置CMS的Initial Mark的时刻导致,根据JVM自己的算法,把CMS启动时间搞到90% 多的HEAP时刻,导致回收失败总是触发Full GC导致,后来通过硬性配置 CMS的Initial Mark 解决了那个毛刺的问题。看上图可知,这次明显不是GC惹的祸。
从图中可以看到,在偶然的一些时刻,会有非常长时间的STW时间(1.5秒),而从下图看到,虽然这次的STW是由GC触发的(GenCollectForAllocation),但是时间杀手并不是GC, 从下图打印的SafePoint statistics 的table看到,时间都花在了 block阶段,也就是在等待JVM线程进入安全点导致,而GC仅仅用了116毫秒。
网上google了一把,几乎没有人提问过为啥block很长这种情况,因此只好去挖挖文章,学习一下JVM的SafePoint原理了。
首先是先去了解什么是SafePoint,挖到一篇不错的文章
JVM源码分析之安全点safepoint
简单总结几句就是说,我们通常以为,JVM STW 很多情况都是因为要GC,其实不然,在JVM里需要STW的情况有下面几种
- Garbage collection pauses
- Code deoptimization
- Flushing code cacheClass redefinition (e.g. hot swap or instrumentation)
- Biased lock revocation
- Various debug operation (e.g. deadlock check or stacktrace dump)
而,在STW之前,JVM需要做的第一件事,就是去把所有的JVM Thread block住,专业点来讲,就是在JVM里安排了一个安全的代码区域叫SafePoint,只要线程跑到了这些安全域,就认为挂起这些线程是安全的。这篇文章详细讲解了几种进入安全域的方法,这里就不再赘述了。
那这个进入SafePoint是不是一个好惹的鸟呢,好惹的话哥就不用写这篇文章了,也不会有这么多程序员被拿去祭天了,进入SafePoint的每个阶段代表什么呢,又有哪些手段可以检测呢?(比如我上面贴问题那两张图),接着我又去拜读了江南白衣文章
JVM的Stop The World,安全点,黑暗的地底世界
上面文章降到可以如何去检测这个ApplicationStopTime,并且解释了那几个字段的含义,这里为了好看,也贴一下:
此日志分两段,第一段是时间戳,VM Operation的类型,以及线程概况
total: 安全点里的总线程数
initially_running: 安全点时开始时正在运行状态的线程数
wait_to_block: 在VM Operation开始前需要等待其暂停的线程数
第二行是到达安全点时的各个阶段以及执行操作所花的时间,其中最重要的是vmop
spin: 等待线程响应safepoint号召的时间
block: 暂停所有线程所用的时间
sync: 等于 spin+block,这是从开始到进入安全点所耗的时间,可用于判断进入安全点耗时
cleanup: 清理所用时间
vmop: 真正执行VM Operation的时间
本人语文理解能力比较差,所以当时看到spin 和block,我的理解spin就是JVM发出global SafePoint响应时,所有线程都接收到这个信号的时间,而block 就是所有线程需要跑到安全点的时间。
这就直接导致了我去想问题的方向,最终是错误的,知道后来又找到另外一篇文章的解释
java GC进入safepoint的时间为什么会这么长?
在这篇文章,它是这么介绍的
2.2 Stop The World的四个阶段
根据-XX:+PrintSafepointStatistics –XX:PrintSafepointStatisticsCount=1 参数虚拟机打印的日志文件可以看出,safepoint的执行一共可以分为四个阶段:
- Spin阶段。因为jvm在决定进入全局safepoint的时候,有的线程在安全点上,而有的线程不在安全点上,这个阶段是等待未在安全点上的用户线程进入安全点。
- Block阶段。即使进入safepoint,用户线程这时候仍然是running状态,保证用户不在继续执行,需要将用户线程阻塞。http://blog.csdn.net/iter_zc/article/details/41892567** 这篇bog详细说明了如何将用户线程阻塞。
- Cleanup。这个阶段是JVM做的一些内部的清理工作。
- VM Operation. JVM执行的一些全局性工作,例如GC,代码反优化。
只要分析这个四个阶段,就能知道什么原因导致的STW时间过长。
再加上Ivy今天的指点,发现和我之前的理解是出入非常大的,最后我是这么理解的,Spin阶段其实就是所有线程跑到了SafePoint 的时间(这个时间一般有长有短,具体看你的JVM 线程都在干嘛,比如线程再跑右边界的大for循环,那么就得等了,这也是在网上能搜到的一般程序员遇到的,就是Spin很长导致最后Sync很长),而 block 阶段则是JVM让所有线程 Block锁花费的时间了,具体JVM是怎么让JVM Thread 阻塞的呢?推荐仔细看下面两篇文章:
聊聊JVM(九)理解进入safepoint时如何让Java线程全部阻塞
聊聊JVM(六)理解JVM的safepoint
如果简单几句话描述的话,就是要视乎当前的JVM Thread在跑什么,比如Java Code? 还是Native调用? 等,然后措施就是让这个线程去OS 去load 一个锁住的Page, 这样的话这个线程发送内核命令去Load Page却Load不到,自然而然就挂起了。
那再回过来我们的问题,怀疑的点就变成很窄了,原本这个系统调用是非常迅速的,很可能就是OS在做自己的IO,比如swapping等,进行了进程上下文的切换,导致根本想去响应这个调用,因此再回来时自然这个操作就耗时了(还记得这个问题吗?就是block 用了1.3s)。
由于线上我们并没有配置SafePoint超时和配置SafePoint超时打印线程状态,因此我们也只能猜测了,最后一种可能就是线程在做状态切换时被阻塞了,注意,这里所说的线程状态是JVM里面对应的VMThread ,而不是传统的Java Thread,大家可以看看下面这篇文章:
聊聊JVM(五)从JVM角度理解线程
看个大概吧,具体深入的CPP我也没仔细去看了,又和Ivy讨论了一番,最后得出结论是,这个损耗应该也是计算在spin里面的而不是block 的耗时。
后话
猜测终归是猜测,在实验室环境压测了2天,也没抓到什么罪证,最大的可能的的确确就是OS在进行上下文切换了,看对症下药做点措施再继续观察了。
收官时刻,突然院长又冒出另外一个问题(巨坑啊啊啊啊啊):CMS 如果不配置 Eden比例的话,默认是很小的,终于发现我们的频繁的YGC是有原因的,哈哈哈,今天一并把这个也该了,希望上面遇到的问题也是因为YGC太频繁而导致的。
目前是加上了 -XX:NewRatio=2 了,GC看起来平滑了N倍。
至于这个问题:还是等解决了一切其他问题后再回来看吧。。。。。。。。
(待续)