JVM-SafePoint(安全点)和STW
示例代码
java
public class Test {
public static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable runnable=()->{
for (int i = 0; i < 1000000000; i++) {
num.getAndAdd(1);
}
};
Thread t1 = new Thread(runnable, "test1");
Thread t2 = new Thread(runnable, "test2");
t1.start();
t2.start();
System.out.println("主线程开始休眠" + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println("主线程结束休眠" + System.currentTimeMillis());
System.out.println(num.get());
}
public static long interval(long startTime){
return System.currentTimeMillis() - startTime;
}
}
直接看代码逻辑很简单,启动了两个线程t1和t2,t1和t2线程每次都对num进行10亿次累加,然后主线程休眠1000ms,休眠结束之后主线程立刻打印出当前时刻的num值。但实际真是这样的吗?
我们来看JDK1.8下的运行结果
con
主线程开始休眠1701309377737
Disconnected from the target VM, address: 'localhost:49710', transport: 'socket'
主线程结束休眠1701309421459
2000000000
实际上我们发现主线程并没有在1000ms之后打印出当前累加的num值,而是等到t1,t2线程完成两次10亿次累加之后才进行打印,这里主线程出现了阻塞,阻塞时间为1701309377737-1701309377737=43722ms,大约43秒的时间。远远高出我们代码里面设置的1000ms的睡眠时间。
这里直接说原因,主线程在sleep结束前进入了JVM全局安全点,然后主线程要等待其他线程全部进入安全点,所以主线程被长时间没有进入安全点的其他线程给阻塞了。
前置知识
GC_Root
在Java体系中,可以作为GC_Root的对象包括以下
- 在虚拟机栈帧中局部变量表中引用的对象,比如各个县城被调用的方法堆栈中使用的参数,局部变量等。因为每一个栈帧都是一个方法调用,虚拟机栈中的栈帧说明当前方法在调用中
- 在方法区中类静态属性引用的对象。
- 在方法区中常量引用的对象,比如字符串常量池中的引用。
- 本地方法栈JNI引用的对象
- Java虚拟机内部的引用,比如基本数据类型对应的Class对象,常驻异常对象,bootstrap类加载器
- 所有被synchronized关键字持有的对象(同步锁)
我们可以发现GC_Root对象都是正在使用或者频繁使用的,虚拟机栈对应方法调用的信息、方法区内事常量、类的Class对象信息
JNI是本地方法调用信息、synchronized关键持有的对象说明当前正在同步代码块中。
可达性分析
通过可达性分析我们来JVM中一个对象是否需要被回收。基本思路是以上面的GC_Root对象作为起点开始通过引用关系开始搜索,搜索过程中走过的路径被称为"引用链"。如果某个对象和GC_Root之间没有任何引用链关联,那么就认为从GC_Root到这个对象不可达,说明当前对象没有被使用,可以回收。
四种引用
在可达性分析中,引用关系是一个基础概念,在引用关系的基础上,JVM又对引用进行了更加细致的划分,分别是强引用,软引用,弱引用,虚引用。
- 强引用-StrongReference 是指在程序代码中引用赋值,比如通过代码"User user = new User()"创建一个User类型对象,此时user就是一个强引用。这种代码声明的强引用关系只要存在,在GC的时候都不会回收user引用的对象
- 软引用-SoftReference 用来描述一些有用但不是必须的对象,只要对象被软引用关联,在虚拟机抛出内存异常之前会把软引用对象进行第二次回收,如果这次回收结束之后还是没有足够内存才会抛出异常。可以用来图片缓存,或者一些被经常访问当时更新次数很低的数据本地缓存。
- 弱引用-WeakReference 比软引用更弱的引用类型,被软引用关联的对象在开始进行GC时,不管当前内存是否足够都会进行回收。
- 虚引用-PhantomReference 如果一个对象仅持有虚引用和没有任何引用是一样的,在任何时候都会被GC回收掉。虚引用必须和引用队列一起使用,当GC准备回收一个对象时,如果发现这个对象还有虚引用,会把这个虚引用加入到与之关联的引用队列中。
- 可以用来跟踪对象被GC时的活动,完成一些资源释放等工作。
oopMap
-
在进行GC时虚拟机要通过GC_Root为起点开始找出与GC_Root之间有引用关系的对象。但是如果在GC时找到所有的GC_Root又成了一个问题。现在的方法区和堆内存都是几个G,如果每次GC都是遍历整个方法区和堆内存然后找出所有的GC_Root,再根据找到的GC_Root开始进行可达性分析,那么整个GC周期将会很很长,但是现在的JVM回收都是毫秒级别,JVM肯定是做了其他优化处理。
-
目前所有的垃圾收集器在进行GC_Root枚举时必须要暂停用户线程(类似于STW),GC_Root枚举时整个JVM中的对象之间的引用关系不能改变,不然可达性分析结果的准确性无法保证。
-
这里就是通过oopMap已空间换时间来提升GC_Root枚举时的速度
oopMap会记录栈帧和寄存器中哪些位置是引用以及在类加载完成之后就能计算出对象内对应偏移量上的数据类型。有了oopMap之后,虚拟机就能快速完成GC_Root枚举。但是随之而来一个新的问题,虚拟机在持续运行过程中导致oopMap变化的指令非常多,不能每次运行一条指令都生成对应的oopMap,这样会需要很多空间来存储oopMap。所以引出一个安全点的概念,
安全点-SafePoint
- 虚拟机不会为每条指令都生成oopMap,只是在特定位置 更新oopMap,这些特定位置就是安全点。
- 有了SafePoint之后,用户程序会强制执行到SafePoint之后才会暂停并开始GC。
- SafePoint的选取不能太少,太少导致GC次数过低容易出现OOM,太多会导致虚拟机负载过高。因此SafePoint的选取规则是是否有让程序长时间执行的特征 ,长时间执行的明显特征就是指令序列的复用,比如方法调用 ,循环跳转 ,异常跳转等。
有了安全点之后,就要处理接下里的问题,如果在要进行GC时让所有的线程都执行到最近的安全点停顿下来,这里有两种方案
- 主动式中断 当GC需要中断线程的时候,不直接操作线程而是设置一个标志位,线程在运行过程中不停地区主动轮训这个标志位,一但发现标志位为真线程就在最近的安全点主动中断挂起。
- 线程轮询在代码中会频繁出现,为了保证高效JVM使用了内存保护陷阱的方式,把轮询指令精简到只有一条汇编指令。
- 抢先式中断 虚拟机先把所有用户线程全中断,然后检查当前线程是否中断的地方是否在安全点,如果不在安全点就恢复当前线程执行,直到跑到安全点上。现在没有虚拟机使用抢先式中断。
安全区域-SafeRegion
在用户程序运行过程中安全点可以保证GC没有问题,但是如果用户线程没有执行,比如处于sleep或blocked状态,此时又没有运行到安全点上是不是就永远无法触发GC了。
为了解决这个问题,虚拟机引入了SafeRegion。
SafeRegion是指在一段代码区域中引用关系不会发生变化,那么在这个区域中任何地方开始GC都是安全的。
用户线程执行到SafeRegion区域时会标识自己进入了SafeRegion,此时虚拟机要进行GC时就不会管这些进入SafeRegion的线程,当线程要离开SafeRegion时会检查虚拟机是否完成GC_Root枚举,如果完成就继续执行,如果没有就一直等待虚拟机完成GC_Root枚举之后收到可以离开SafeRegion区域信号之后继续执行。
示例代码增加启动参数
我们在我们来看JDK1.8下并添加启动参数 -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 -XX:+SafepointTimeout -XX:SafepointTimeoutDelay=2000 打印出安全点的信息,得到如下输出。
参数说明
-XX:+SafepointTimeout -XX:SafepointTimeoutDelay=2000 打印进入SafePoint超过2000ms的线程
-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 格式化输出SafePoint相关信息
java
主线程开始休眠1701333883422
# SafepointSynchronize::begin: Timeout detected:
# SafepointSynchronize::begin: Timed out while spinning to reach a safepoint.
# SafepointSynchronize::begin: Threads which did not reach the safepoint:
# "test2" #14 prio=5 os_prio=31 tid=0x00000001430f2800 nid=0x5c03 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
# "test1" #13 prio=5 os_prio=31 tid=0x0000000142949800 nid=0x5a03 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
# SafepointSynchronize::begin: (End of list)
Disconnected from the target VM, address: 'localhost:49951', transport: 'socket'
主线程结束休眠1701333939906
2000000000
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
0.027: ForceSafepoint [ 7 0 0 ] [ 0 0 0 0 0 ] 0
0.028: ChangeBreakpoints [ 7 0 0 ] [ 0 0 0 0 0 ] 0
1.033: no vm operation [ 14 2 2 ] [ 55517 0 55517 0 0 ] 0
56.550: EnableBiasedLocking [ 14 0 0 ] [ 0 0 0 0 0 ] 0
56.550: no vm operation [ 9 1 1 ] [ 0 0 0 0 0 ] 0
Polling page always armed
ForceSafepoint 1
EnableBiasedLocking 1
ChangeBreakpoints 1
0 VM operations coalesced during safepoint
Maximum sync time 55517 ms
Maximum vm operation time (except for Exit VM operation) 0 ms
输出解析
- 第一列是当前打印日志时间点,它是相对于进程启动后经过的秒数。
- 第二列是触发safepoint机制的操作,比如no vm operation操作
- 第三列是当前JVM线程情况
- total STW发生时当前JVM的线程数量
- initially_running STW时仍在运行的线程数量,这是spin阶段的时间来源
- wait_to_block STW需要阻塞的线程数量,这是block阶段的主要来源
- 第四列是safepoint各阶段的耗时
- spin:因为jvm在决定进入全局safepoint的时候,有的线程在安全点上,而有的线程不在安全点上,这个阶段是垃圾收集线程等待未在安全点上的线程进入安全点的时间。
- block:即使进入safepoint,后进入safepoint的部分线程可能仍然是running状态,这是等待它们阻塞起来消耗的时间。
- sync:等于spin + block + 其它活动耗时,gc的STW日志最后的
Stopping threads took
等于spin + block。 - cleanup:这个阶段是JVM做的一些内部的清理工作。
- vmop:实际safepoint操作花费的时间
EnableBiasedLocking JVM开启偏向锁的操作。
我们可以看到JVM想要no vm operation操作,而执行这个操作需要线程进入安全点,这一刻虚拟机一共有12个线程,其中正在运行的有两个线程,等这两个线程进入安全点耗时55517ms。这两个线程就是test1和test2线程,就是自循环10亿次的两个线程。
关于no vm operation
这里解析一下no vm operation操作,通过在JDK8虚拟机源码里面我们搜到了如下
c++
// safepoint.cpp中
void SafepointSynchronize::print_statistics() {
SafepointStats* sstats = _safepoint_stats;
for (int index = 0; index <= _cur_stat_index; index++) {
if (index % 30 == 0) {
print_header();
}
sstats = &_safepoint_stats[index];
tty->print("%.3f: ", sstats->_time_stamp);
tty->print("%-26s ["
INT32_FORMAT_W(8) INT32_FORMAT_W(11) INT32_FORMAT_W(15)
" ] ",
sstats->_vmop_type == -1 ? "no vm operation" :
VM_Operation::name(sstats->_vmop_type),
sstats->_nof_total_threads,
sstats->_nof_initial_running_threads,
sstats->_nof_threads_wait_to_block);
tty->print(" ["
INT64_FORMAT_W(6) INT64_FORMAT_W(6)
INT64_FORMAT_W(6) INT64_FORMAT_W(6)
INT64_FORMAT_W(6) " ] ",
sstats->_time_to_spin / MICROUNITS,
sstats->_time_to_wait_to_block / MICROUNITS,
sstats->_time_to_sync / MICROUNITS,
sstats->_time_to_do_cleanups / MICROUNITS,
sstats->_time_to_exec_vmop / MICROUNITS);
if (need_to_track_page_armed_status) {
tty->print(INT32_FORMAT " ", sstats->_page_armed);
}
tty->print_cr(INT32_FORMAT " ", sstats->_nof_threads_hit_page_trap);
}
}
c++
// safepoint.cpp中
void SafepointSynchronize::begin_statistics(int nof_threads, int nof_running) {
assert(init_done, "safepoint statistics array hasn't been initialized");
SafepointStats *spstat = &_safepoint_stats[_cur_stat_index];
spstat->_time_stamp = _ts_of_current_safepoint;
VM_Operation *op = VMThread::vm_operation();
spstat->_vmop_type = (op != NULL ? op->type() : -1);
if (op != NULL) {
_safepoint_reasons[spstat->_vmop_type]++;
}
spstat->_nof_total_threads = nof_threads;
spstat->_nof_initial_running_threads = nof_running;
spstat->_nof_threads_hit_page_trap = 0;
if (nof_running != 0) {
spstat->_time_to_spin = os::javaTimeNanos();
} else {
spstat->_time_to_spin = 0;
}
}
主线程为什么进入安全点
到这里我们已经确定因为主线程进入安全点,但是test1和test2线程没有执行完毕,主线程一直等待test1和test2执行到安全点。那么问题来了主线程为什么安全点。
在JVM正常运行时,如果设置了进入安全点的时间间隔,那么每隔指定的时间间隔都会判断是否要进入安全点。这个触发条件不是VM操作,所以会将 _vmop_type 设置成-1,所以输出日志就是no vm operation就是我们看到的安全点日志。
在VM操作为空的情况下,满足以下条件也会进入安全点
- VMThread处于正常运行状态
- 设置了进入安全点的时间间隔
- SafepointALot 是否为 true 或者是否需要清理
通过Java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal 2>&1 | grep Safepoint查看当前JVM关于安全点参数如下
bash
bool AbortVMOnSafepointTimeout = false {diagnostic}
bool DebugNonSafepoints = false {diagnostic}
intx GuaranteedSafepointInterval = 1000 {diagnostic}
bool PrintSafepointStatistics = false {product}
intx PrintSafepointStatisticsCount = 300 {product}
intx PrintSafepointStatisticsTimeout = -1 {product}
intx SafepointPollOffset = 0 {C1 pd product}
intx SafepointSpinBeforeYield = 2000 {product}
bool SafepointTimeout = false {product}
intx SafepointTimeoutDelay = 10000 {product}
bool TraceSafepointCleanupTime = false {product}
bool UseCompilerSafepoints = true {product}
bool UseCountedLoopSafepoints = false {C2 product}
我们可以看到发现 GuaranteedSafepointInterval 默认设置成了 1 秒,每隔1s就会尝试进入安全点。同时UseCountedLoopSafepoints=false关闭了可数循环的安全点设置。
我们尝试修改GuaranteedSafepointInterval看看是否能够阻止进入安全点
通过增加启动参数 -XX:+UnlockDiagnosticVMOptions -XX:GuaranteedSafepointInterval=0 得到如下输出
java
主线程开始休眠1701351535022
主线程结束休眠1701351536024
54274878
我们看到主线程休眠1000ms之后立刻打印了当前num值。
主线程从哪里进入SafePoint 安全点
在JVM源码查no vm operation信息时在safepoint.cpp的代码中找到一部分注释如下
c
// Begin the process of bringing the system to a safepoint.
// Java threads can be in several different states and are
// stopped by different mechanisms:
//
// 1. Running interpreted
// The interpeter dispatch table is changed to force it to
// check for a safepoint condition between bytecodes.
// 2. Running in native code
// When returning from the native code, a Java thread must check
// the safepoint _state to see if we must block. If the
// VM thread sees a Java thread in native, it does
// not wait for this thread to block. The order of the memory
// writes and reads of both the safepoint state and the Java
// threads state is critical. In order to guarantee that the
// memory writes are serialized with respect to each other,
// the VM thread issues a memory barrier instruction
// (on MP systems). In order to avoid the overhead of issuing
// a memory barrier for each Java thread making native calls, each Java
// thread performs a write to a single memory page after changing
// the thread state. The VM thread performs a sequence of
// mprotect OS calls which forces all previous writes from all
// Java threads to be serialized. This is done in the
// os::serialize_thread_states() call. This has proven to be
// much more efficient than executing a membar instruction
// on every call to native code.
// 3. Running compiled Code
// Compiled code reads a global (Safepoint Polling) page that
// is set to fault if we are trying to get to a safepoint.
// 4. Blocked
// A thread which is blocked will not be allowed to return from the
// block condition until the safepoint operation is complete.
// 5. In VM or Transitioning between states
// If a Java thread is currently running in the VM or transitioning
// between states, the safepointing code will wait for the thread to
// block itself when it attempts transitions to a new state.
//
这段注释的主要内容是介绍JVM在java线程不同状态时进入安全点如何处理,翻译如下:
线程虚拟机开始将系统进入SafePoint时,Java线程会处于不同的状态,每种状态会由不同的机制尝试停止
-
运行字节码时,要执行下一条指令时会检查字节码之间的安全点条件。
-
这里解释一下,开始我也不清楚interpeter dispatch table的含义,我在JVM源码中搜索了一下,
c++class DispatchTable VALUE_OBJ_CLASS_SPEC { public: enum { length = 1 << BitsPerByte }; // an entry point for each byte value (also for undefined bytecodes) private: address _table[number_of_states][length]; // dispatch tables, indexed by tosca and bytecode 这个表明字节码索引 public: // Attributes EntryPoint entry(int i) const; // return entry point for a given bytecode i void set_entry(int i, EntryPoint& entry); // set entry point for a given bytecode i address* table_for(TosState state) { return _table[state]; } address* table_for() { return table_for((TosState)0); } int distance_from(address *table) { return table - table_for(); } int distance_from(TosState state) { return distance_from(table_for(state)); } // Comparison bool operator == (DispatchTable& y); // for debugging only };
这部分代码在templateInterpreter.hpp中,在JVM中templateInterpreter是一个模板解释器,用来处理字节码;同时根据上面的注释综合猜测出来的
-
-
在从本地代码(native code)返回时,Java线程必须检查SafePoint状态以确定是否需要阻塞。如果VM线程看到一个Java线程正在执行本地代码,它不会等待该线程阻塞。这里关键是对safepoint状态和Java线程状态的内存写入和读取顺序。为了确保内存写入彼此之间是有序的,在多处理器(MP)系统上,VM线程会发出内存屏障指令。为了避免每个Java线程在进行本地调用时都执行内存屏障指令的开销,每个Java线程在更改线程状态后都会对单个内存页执行写入。VM线程执行一系列的mprotect操作,强制所有先前的写入来自所有Java线程都进行序列化。这是在
os::serialize_thread_states()
调用中完成的。这种方法被证明比在每次调用本地代码时执行内存屏障指令要高效得多。 -
正在运行JTI编译代码 此时JTI代码要读取一个Safepoint Polling(安全点轮询)页面是否设置为错误,如果是的话尝试进入SafePoint
-
正在处于blocked状态的线程在安全点操作完成之前不能从blocked状态返回
-
如果一个 Java 线程当前正在虚拟机中运行或者正在状态转换中,安全点代码将等待该线程在尝试切换到新状态时自行阻塞。
实际上在我们示例代码中休眠主线程是通过Thread.sleep(1000)调用返回之后完成的主线程挂起操作,因为sleep就是一个native本地方法。整个流程时间线如下
- 代码执行,JVM启动,此时JVM参数GuaranteedSafepointInterval=1000ms,意味着JVM启动之后每隔1000ms设置一个安全点标识位,让所有用户线程运行到最近的安全点开始更新oopMap,查看是否需要进行GC。
- 在不到1000ms的时间内,main线程完成创建test1和test2线程并调用start启动test1和test2线程,同时调用Thead.sleep(1000)休眠自己,这一阶段消耗300ms(这个时间不准确,主要通过时间前进来推动流程)。
- 到了JVM启动1000ms时,JVM内部线程设置了一个安全点标识位。
- 1300ms时,main线程结束休眠从sleep方法返回并检查安全点标识位,发现已经被设置,所以挂起自己,等待其他线程走到最近的安全点开始更新oopMap。(对应上面第三条---正在运行JTI编译代码 此时JTI代码要读取一个Safepoint Polling(安全点轮询)页面是否设置为错误,如果是的话尝试进入SafePoint)
- 此时test1和test2线程因为在进行可数循环没有设置安全点,所以必须等到test1和test2执行完10亿次之后,执行下一条字节码指令时开始检查安全点;然后走到最近安全点挂起之后更新oopMap,然后main线程继续执行。
- 这里因为JVM对可数循环的优化导致了两个test1和test2线程必须要执行完10亿次的循环才能走到最近的安全点。
- 可数循环是使用int类型或者表示范围更小的数据类型作为索引值进行循环时默认是不会放置安全点的,JVM判断这个循环次数受到数据类型表示范围的影响执行时间不会很长。
- 使用long类型或者范围更大的数据类型作为索引值的循环会认为是不可数循环会放置安全点。
- 这里会有一个问题,如果是10次循环,但是如果循环里面涉及到了耗时操作,比如三方调用超时或者每次循环里面又调用了其他方法的耗时操作也会到导致这个问题。
JIT优化的影响
示例代码中 **num.getAndAdd(1);**实际上也调用了native方法,为什么在方法返回的时候没有轮询安全点标识呢?
因为这里被JIT给优化了,通过**-Djava.compiler=NONE**关闭JIT编译优化之后main线程在睡眠1000ms之后也立即结束了。
什么时候进入安全点
- 由于 jstack,jmap 和 jstat 等命令,会导致 Stop the world:这种命令都需要采集堆栈信息,所以需要所有线程进入 Safepoint 并暂停。
- 定时进入 SafePoint:每经过
-XX:GuaranteedSafepointInterval
配置的时间,都会让所有线程进入 Safepoint,一旦所有线程都进入,立刻从 Safepoint 恢复。这个定时主要是为了一些没必要立刻 Stop the world 的任务执行,可以设置-XX:GuaranteedSafepointInterval=0
关闭这个定时。 - Java Code Cache相关:当发生 JIT 编译优化或者去优化,需要 OSR 或者 Bailout 或者清理代码缓存的时候,由于需要读取线程执行的方法以及改变线程执行的方法,所以需要进入SafePoint
- Java agent导致类重定义,需要修改栈上和这个类相关的信息,所以需要进入SafePoint
- 偏向锁取消,高并发的情况下,偏向锁会经常失效,导致需要取消偏向锁,取消偏向锁的时候,需要 Stop the world,因为要获取每个线程使用锁的状态以及运行状态。
安全点的副作用
SafePoint就是为了让用户线程进入STW然后更新oopMap为GC做准备,STW对应用服务系统来说很可怕。
安全点最主要的副作用就是可能导致STW时间过长,应该极力避免这点副作用。
对第一个进入安全点的线程来说,STW是从它进入安全点开始的,如果有某个线程一直无法进入安全点就会导致进入安全点的时间一直处于等待状态,进而导致STW的时间过长。所以,应避免线程执行过长无法进入安全点的情况。
- 可数循环体内执行时间过长以及JIT优化导致无法进入安全点的问题是最常见的无法进入安全点的情况。在写大循环的时候可以把循环索引值数据类型定义成long。
- 在高并发应用中,偏向锁并不能带来性能提升,反而因为偏向锁取消带来了很多没必要的某些线程进入安全点 。所以建议关闭:
-XX:-UseBiasedLocking
。 - jstack,jmap 和 jstat 等命令,也会导致进入安全点。所以,生产环境应该关闭Thead dump的开关,避免dump时间过长导致应用STW时间过长。
解决最初问题
循环内部调用一次本地方法
java
public class Test {
public static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable runnable=()->{
for (int i = 0; i < 1000000000; i++) {
num.getAndAdd(1);
if (i == 100000){
Thread.yield();
}
}
};
Thread t1 = new Thread(runnable, "test1");
Thread t2 = new Thread(runnable, "test2");
t1.start();
t2.start();
System.out.println("主线程开始休眠" + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println("主线程结束休眠" + System.currentTimeMillis());
System.out.println(num.get());
}
public static long interval(long startTime){
return System.currentTimeMillis() - startTime;
}
}
比如在第100000次调用一下Thread.yield()这个本地方法,main也不会阻塞这么久
JDK11已经默认开始开启可数循环的安全点设置
在JDK11中可数循环安全点设置默认开启。通过 Java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal 2>&1 | grep Safepoint我们可以看到JDK11中
UseCountedLoopSafepoints=true。所以最开始的示例代码在JDK11运行不会出现main线程卡顿那么久。
java
bool AbortVMOnSafepointTimeout = false {diagnostic} {default}
bool DebugNonSafepoints = false {diagnostic} {default}
intx GuaranteedSafepointInterval = 1000 {diagnostic} {default}
bool PrintSafepointStatistics = false {product} {default}
intx PrintSafepointStatisticsCount = 300 {product} {default}
intx PrintSafepointStatisticsTimeout = -1 {product} {default}
bool SafepointALot = false {diagnostic} {default}
bool SafepointTimeout = false {product} {default}
intx SafepointTimeoutDelay = 10000 {product} {default}
bool UseCountedLoopSafepoints = true {C2 product} {default}
改成不可数循环
直接循环数据类型由int改为long,jvm会认为这是一个不可数循环,会放置安全点;main线程也会在休眠1000ms之后继续执行。
java
public class Test {
public static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable runnable=()->{
for (long i = 0; i < 1000000000; i++) {
num.getAndAdd(1);
if (i == 100000){
Thread.yield();
}
}
};
Thread t1 = new Thread(runnable, "test1");
Thread t2 = new Thread(runnable, "test2");
t1.start();
t2.start();
System.out.println("主线程开始休眠" + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println("主线程结束休眠" + System.currentTimeMillis());
System.out.println(num.get());
}
public static long interval(long startTime){
return System.currentTimeMillis() - startTime;
}
}