。原文首发于知乎https://www.zhihu.com/question/57722838
1 SafePoint
1.1 什么是SafePoint
Safepoint是java代码中一个线程可能暂停执行的一个位置,SafePoint保存了其他位置没有的一些运行信息。在这个位置上保存了线程上下文的任何信息,包括对象或者非对象的内部指针。在接收到JVM的进入Stop The World 的信息,在safepoint上,用户线程会被挂起。如果JNI代码想要访问在安全点上的用户线程,那么JNI所在线程也会阻塞直到用户线程离开安全点。因为在安全点上jvm线程信息被很好的描述,所以特别适合做一些全局性的操作,例如代码反优化,线程快照等等。http://peg.hengtiansoft.com/article/jvm-zhong-de-safepoint/
1.2 Safepoint是如何工作的?
在HotSpot虚拟机中,safepoint 协议是主动协作的 。每一个用户线程在安全点 上都会检测一个标志位,来决定自己是否暂停执行。
对于JIT编译后的代码,JIT会在代码特定的位置(通常来说,在方法的返回处和counted loop的结束处)上插入安全点代码。对于解释执行的代码,JVM会设置一个2字节的dispatch tables,解释器执行的时候会经常去检查这个dispatch tables,当有safepoint请求的时候,就会让线程去进行safepoint检查。
1.3 Safepoint的相关总结
翻译自: http://chriskirk.blogspot.com/2013/09/what-is-java-safepoint.html
想了解SafePoint源码的同学,请参照这里:http://hg.openjdk.java.net/jdk7u/jdk7u/hotspot/file/tip/src/share/vm/runtime/safepoint.cpp
2 Stop The World
2.1 什么是Stop the World
Stop the World 是JVM等待所有的用户线程进入safepoint并且阻塞,做一些全局性操作的行为。进入STW的原因不仅仅是因为GC,还有其他原因:
更加详细的参考(很长的一个列表)
http://hg.openjdk.java.net/jdk7/jdk7/hotspot/file/9b0ca45cd756/src/share/vm/runtime/vm_operations.hpp
2.2 Stop The World的四个阶段
根据-XX:+PrintSafepointStatistics –XX:PrintSafepointStatisticsCount=1 参数虚拟机打印的日志文件可以看出,safepoint的执行一共可以分为四个阶段:
只要分析这个四个阶段,就能知道什么原因导致的STW时间过长。
3 STW相关参数
3.1 -XX:+PrintGCApplicationStoppedTime
这个参数用于记录程序运行期间所有情况引发的STW,打开以后,GC日志文件会文件会出现类似于如下内容:
2017-03-31T20:34:54.790+0000: 306738.054: Total time for which application threads were stopped: 0.0434914 seconds, Stopping threads took: 0.0023843 seconds
2017-03-31T20:34:54.790+0000: 306738.054: Application time: 0.0003778 seconds
重点解释一下两个参数:第一个黑体数字306738.054是JVM启动后的秒数,第二个黑字体0.0434914是 JVM发起STW的开始到结束的时间。特别地,如果是GC引发的STW,这条内容会紧挨着出现在GC log的下面,如下:
3.2 -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1
这两个参数用于记录STW发生的原因、线程情况、STW各个阶段的停顿时间等。当添加这两个参数后,程序运行期间会打印如下内容(答主的环境会打印到一个blobsvc.out文件):
如上内容和GC log内容中包含的:Total time for which application threads…一一对应。更加详细的对应关系如下图所示:
参数介绍:
分析blobsvc.out文件,发现好多STW的原因都是RevokeBias或者BulkRevokeBias。
这个是撤销偏向锁操作,虽然每次暂停的 时间很短,但是特别频繁出现。偏向锁相关内容请参考:http://www.cnblogs.com/paddix/p/5405678.html 所以在一些高并发的系统中,禁掉JVM偏向锁优化,可以提升系统的吞吐量。禁用偏向锁的参数为: -XX:-UseBiasedLocking
4 引发长时间STW的Troubleshooting
引发STW时间过长的原因答主能确定的有如下几个:
还有一个引发长时间的STW 原因是因为程序中存在非常长的Count Looop并且循环中没有执行其他函数,或者执行的函数被内联进来。这种程序会导致Spin的时间变的很长,JVM要等待程序运行完循环之才进入STW(通常在方法的返回处和counted loop的结束处插入SafePoint) 导致SWT时间变长.这个问题R大也曾经讨论过:http://hllvm.group.iteye.com/group/topic/38232
@RednaxelaFX
可惜这个猜想答主并不能重现。答主在执行10w次1亿的长循环,Spin时间基本在5毫秒以下,和预想的结果很不一样。希望R大能够指点一下。
答主的测试程序如下:
class LongCount implements Runnable {
public void run() {
long k = 0;
System.out.println("开始执行");
for(int j =0;j<100000;j++)
for(long i = 0;i<100000000L;i++){
k++;
}
System.out.println("执行结束");
System.exit(0);
}
}
public class SafePointTest {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
while(true){
new Object();
}
}
}).start();
new Thread(new LongCount()).start();
}
}
JVM参数为:
-XX:+ExplicitGCInvokesConcurrent
-XX:G1HeapRegionSize=2m
-XX:GCLogFileSize=52428800
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=C:\Users\chenc49\Desktop\log\1.txt
-XX:InitialHeapSize=50m
-XX:MaxHeapSize=50m
-XX:InitiatingHeapOccupancyPercent=30
-XX:MaxGCPauseMillis=50
-XX:MaxHeapSize=256m
-XX:MaxTenuringThreshold=8
-XX:NumberOfGCLogFiles=3
-XX:+PrintAdaptiveSizePolicy
-XX:+PrintGC
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
-XX:+PrintTenuringDistribution
-XX:+UnlockExperimentalVMOptions
-XX:+UseCompressedOops
较小堆空间可以保证频繁进入STW进行GC。答主在JDK6和8的运行环境得到的结果都是一样的:Spin的时间非常短。
不知道是不是我测试程序写的有问题,如果能重现长时间的Spin,希望大家能够贴出来。
答主提的问题,基本上已经没办法重现,但是如果再次出现total time for which application threads were stopped 时间很长的情况,这个回答,也算一个参考的思路.
如果有错误不足之处,还请各位批评指出哈