SafePoint 与 Stop The World 全解 (基于 OpenJDK 11 版本)

在分析线上 JVM 性能问题的时候,咱们可能会碰到下面这些场景:java

1.GC 自己没有花多长时间,可是 JVM 暂停了好久,例以下面:git

github

缓存

2.JVM 没有 GC,可是程序暂停了好久,并且这种状况时不时就出现。性能优化

这些问题通常和 SafePoint 还有 Stop the World 有关。多线程

什么是 SafePoint?什么是 Stop the world?他们之间有何关系?

咱们先来设想下以下场景:并发

  1. 当须要 GC 时,须要知道哪些对象还被使用,或者已经不被使用能够回收了,这样就须要每一个线程的对象使用状况。app

  2. 对于偏向锁(Biased Lock),在高并发时想要解除偏置,须要线程状态还有获取锁的线程的精确信息。微服务

  3. 对方法进行即时编译优化(OSR栈上替换),或者反优化(bailout栈上反优化),这须要线程究竟运行到方法的哪里的信息。高并发

对于这些操做,都须要线程的各类信息,例如寄存器中到底有啥,堆使用信息以及栈方法代码信息等等等等,而且作这些操做的时候,线程须要暂停,等到这些操做完成,不然会有并发问题。这就须要 SafePoint。

Safepoint 能够理解成是在代码执行过程当中的一些特殊位置,当线程执行到这些位置的时候,线程能够暂停。在 SafePoint 保存了其余位置没有的一些当前线程的运行信息,供其余线程读取。这些信息包括:线程上下文的任何信息,例如对象或者非对象的内部指针等等。咱们通常这么理解 SafePoint,就是线程只有运行到了 SafePoint 的位置,他的一切状态信息,才是肯定的,也只有这个时候,才知道这个线程用了哪些内存,没有用哪些;而且,只有线程处于 SafePoint 位置,这时候对 JVM 的堆栈信息进行修改,例如回收某一部分不用的内存,线程才会感知到,以后继续运行,每一个线程都有一份本身的内存使用快照,这时候其余线程对于内存使用的修改,线程就不知道了,只有再进行到 SafePoint 的时候,才会感知

因此,GC 必定须要全部线程同时进入 SafePoint,并停留在那里,等待 GC 处理完内存,再让全部线程继续执。像这种**全部线程进入 SafePoint **等待的状况,就是 Stop the world(此时,忽然想起承太郎的:食堂泼辣酱,the world!!!)。

为何须要 SafePoint 以及 Stop The World?

在 SafePoint 位置保存了线程上下文中的任何东西,包括对象,指向对象或非对象的内部指针,在线程处于 SafePoint 的时候,对这些信息进行修改,线程才能感知到。因此,只有线程处于 SafePoint 的时候,才能针对线程使用的内存进行 GC,以及改变正在执行的代码,例如 OSR (On Stack Replacement,栈上替换现有代码为JIT优化过的代码)或者 Bailout(栈上替换JIT过优化代码为去优化的代码)。而且,还有一个重要的 Java 线程特性也是基于 SafePoint 实现的,那就是 Thread.interrupt(),线程只有运行到 SafePoint 才知道是否 interrupted。

为啥须要 Stop The World,有时候咱们须要全局全部线程进入 SafePoint 这样才能统计出那些内存还能够回收用于 GC,,以及回收再也不使用的代码清理 CodeCache,以及执行某些 Java instrument 命令或者 JDK 工具,例如 jstack 打印堆栈就须要 Stop the world 获取当前全部线程快照。

SafePoint 如何实现的?

能够这么理解,SafePoint 能够插入到代码的某些位置,每一个线程运行到 SafePoint 代码时,主动去检查是否须要进入 SafePoint,这个主动检查的过程,被称为 Polling

理论上,能够在每条 Java 编译后的字节码的边界,都放一个检查 Safepoint 的机器命令。线程执行到这里的时候,会执行 Polling 询问 JVM 是否须要进入 SafePoint,这个询问是会有性能损耗的,因此 JIT 会优化尽可能减小 SafePoint。

通过 JIT 编译优化的代码,会在全部方法的返回以前,以及全部非counted loop的循环(无界循环)回跳以前放置一个 SafePoint,为了防止发生 GC 须要 Stop the world 时,该线程一直不能暂停,可是对于明确有界循环,为了减小 SafePoint,是不会在回跳以前放置一个 SafePoint,也就是:

for (int i = 0; i < 100000000; i++) {
    ...
}

里面是不会放置 SafePoint 的,这也致使了后面会提到的一些性能优化的问题。注意,仅针对 int 有界循环,例如里面的 int i 换成 long i 就仍是会有 SafePoint

SafePoint 实现相关源代码: safepoint.cpp

能够看出,针对 SafePoint,线程有 5 种状况;假设如今有一个操做触发了某个 VM 线程全部线程须要进入 SafePoint(例如如今须要 GC),若是其余线程如今:

  • 运行字节码:运行字节码时,解释器会看线程是否被标记为 poll armed,若是是,VM 线程调用 SafepointSynchronize::block(JavaThread *thread)进行 block。

  • 运行 native 代码:当运行 native 代码时,VM 线程略过这个线程,可是给这个线程设置 poll armed,让它在执行完 native 代码以后,它会检查是否 poll armed,若是还须要停在 SafePoint,则直接 block。

  • 运行 JIT 编译好的代码:因为运行的是编译好的机器码,直接查看本地 local polling page 是否为脏,若是为脏则须要 block。这个特性是在 Java 10 引入的 JEP 312: Thread-Local Handshakes 以后,才是只用检查本地 local polling page 是否为脏就能够了。

  • 处于 BLOCK 状态:在须要全部线程须要进入 SafePoint 的操做完成以前,不准离开 BLOCK 状态

  • 处于线程切换状态或者处于 VM 运行状态:会一直轮询线程状态直到线程处于阻塞状态(线程确定会变成上面说的那四种状态,变成哪一个都会 block 住)。

哪些状况下会让全部线程进入 SafePoint, 即发生 Stop the world?

  1. 定时进入 SafePoint:每通过-XX:GuaranteedSafepointInterval 配置的时间,都会让全部线程进入 Safepoint,一旦全部线程都进入,马上从 Safepoint 恢复。这个定时主要是为了一些不必马上 Stop the world 的任务执行,能够设置-XX:GuaranteedSafepointInterval=0关闭这个定时,我推荐是关闭。

  2. 因为 jstack,jmap 和 jstat 等命令,也就是 Signal Dispatcher 线程要处理的大部分命令,都会致使 Stop the world:这种命令都须要采集堆栈信息,因此须要全部线程进入 Safepoint 并暂停。

  3. 偏向锁取消(这个不必定会引起总体的 Stop the world,参考JEP 312: Thread-Local Handshakes):Java 认为,锁大部分状况是没有竞争的(某个同步块大多数状况都不会出现多线程同时竞争锁),因此能够经过偏向来提升性能。即在无竞争时,以前得到锁的线程再次得到锁时,会判断是否偏向锁指向我,那么该线程将不用再次得到锁,直接就能够进入同步块。可是高并发的状况下,偏向锁会常常失效,致使须要取消偏向锁,取消偏向锁的时候,须要 Stop the world,由于要获取每一个线程使用锁的状态以及运行状态。

  4. Java Instrument 致使的 Agent 加载以及类的重定义:因为涉及到类重定义,须要修改栈上和这个类相关的信息,因此须要 Stop the world

  5. Java Code Cache相关:当发生 JIT 编译优化或者去优化,须要 OSR 或者 Bailout 或者清理代码缓存的时候,因为须要读取线程执行的方法以及改变线程执行的方法,因此须要 Stop the world

  6. GC:这个因为须要每一个线程的对象使用信息,以及回收一些对象,释放某些堆内存或者直接内存,因此须要 Stop the world

  7. JFR 的一些事件:若是开启了 JFR 的 OldObject 采集,这个是定时采集一些存活时间比较久的对象,因此须要 Stop the world。同时,JFR 在 dump 的时候,因为每一个线程都有一个 JFR 事件的 buffer,须要将 buffer 中的事件采集出来,因此须要 Stop the world。

其余的事件,不常常遇到,能够参考源码 vmOperations.hpp

#define VM_OPS_DO(template)                       \
  template(None)                                  \
  template(Cleanup)                               \
  template(ThreadDump)                            \
  template(PrintThreads)                          \
  template(FindDeadlocks)                         \
  template(ClearICs)                              \
  template(ForceSafepoint)                        \
  template(ForceAsyncSafepoint)                   \
  template(DeoptimizeFrame)                       \
  template(DeoptimizeAll)                         \
  template(ZombieAll)                             \
  template(Verify)                                \
  template(PrintJNI)                              \
  template(HeapDumper)                            \
  template(DeoptimizeTheWorld)                    \
  template(CollectForMetadataAllocation)          \
  template(GC_HeapInspection)                     \
  template(GenCollectFull)                        \
  template(GenCollectFullConcurrent)              \
  template(GenCollectForAllocation)               \
  template(ParallelGCFailedAllocation)            \
  template(ParallelGCSystemGC)                    \
  template(G1CollectForAllocation)                \
  template(G1CollectFull)                         \
  template(G1Concurrent)                          \
  template(G1TryInitiateConcMark)                 \
  template(ZMarkStart)                            \
  template(ZMarkEnd)                              \
  template(ZRelocateStart)                        \
  template(ZVerify)                               \
  template(HandshakeOneThread)                    \
  template(HandshakeAllThreads)                   \
  template(HandshakeFallback)                     \
  template(EnableBiasedLocking)                   \
  template(BulkRevokeBias)                        \
  template(PopulateDumpSharedSpace)               \
  template(JNIFunctionTableCopier)                \
  template(RedefineClasses)                       \
  template(UpdateForPopTopFrame)                  \
  template(SetFramePop)                           \
  template(GetObjectMonitorUsage)                 \
  template(GetAllStackTraces)                     \
  template(GetThreadListStackTraces)              \
  template(GetFrameCount)                         \
  template(GetFrameLocation)                      \
  template(ChangeBreakpoints)                     \
  template(GetOrSetLocal)                         \
  template(GetCurrentLocation)                    \
  template(ChangeSingleStep)                      \
  template(HeapWalkOperation)                     \
  template(HeapIterateOperation)                  \
  template(ReportJavaOutOfMemory)                 \
  template(JFRCheckpoint)                         \
  template(ShenandoahFullGC)                      \
  template(ShenandoahInitMark)                    \
  template(ShenandoahFinalMarkStartEvac)          \
  template(ShenandoahInitUpdateRefs)              \
  template(ShenandoahFinalUpdateRefs)             \
  template(ShenandoahDegeneratedGC)               \
  template(Exit)                                  \
  template(LinuxDllLoad)                          \
  template(RotateGCLog)                           \
  template(WhiteBoxOperation)                     \
  template(JVMCIResizeCounters)                   \
  template(ClassLoaderStatsOperation)             \
  template(ClassLoaderHierarchyOperation)         \
  template(DumpHashtable)                         \
  template(DumpTouchedMethods)                    \
  template(PrintCompileQueue)                     \
  template(PrintClassHierarchy)                   \
  template(ThreadSuspend)                         \
  template(ThreadsSuspendJVMTI)                   \
  template(ICBufferFull)                          \
  template(ScavengeMonitors)                      \
  template(PrintMetadata)                         \
  template(GTestExecuteAtSafepoint)               \
  template(JFROldObject)                          \

什么状况会致使 Stop the world 时间过长?

Stop the world 阶段能够简单分为(这段时间内,JVM 都是出于全部线程进入 Safepoint 就 block 的状态):

  1. 某个操做,须要 Stop the world(就是上面提到的哪些状况下会让全部线程进入 SafePoint, 即发生 Stop the world 的那些操做)

  2. 向 Signal Dispatcher 这个 JVM 守护线程发起 Safepoint 同步信号并交给对应的模块执行。

  3. 对应的模块,采集全部线程信息,并对每一个线程根据状态作不一样的操做以及标记(根据以前源代码那一块的描述,有5种状况)

  4. 全部线程都进入 Safepoint 并 block。

  5. 作须要发起 Stop the world 的操做。

  6. 操做完成,全部线程从 Safepoint 恢复。

基于这些阶段,致使 Stop the world 时间过长的缘由有:

  1. 阶段 4 耗时过长,即等待全部线程中的某些线程进入 Safepoint 的时间过长,这个极可能和有 大有界循环与JIT优化 有关,也极可能是 OpenJDK 11 引入的获取调用堆栈的类StackWalker的使用致使的,也多是系统 CPU 资源问题或者是系统内存脏页过多或者发生 swap 致使的。

  2. 阶段 5 耗时过长,须要看看是哪些操做致使的,例如偏向锁撤销过多, GC时间过长等等,须要想办法减小这些操做消耗的时间,或者直接关闭这些事件(例如关闭偏向锁,关闭 JFR 的 OldObjectSample 事件采集)减小进入,这个和本篇内容无关,这里不赘述。

  3. 阶段2,阶段3耗时过长,因为 Signal Dispatcher 是单线程的,能够看看当时 Signal Dispatcher 这个线程在干什么,多是 Signal Dispatcher 作其余操做致使的。也多是系统 CPU 资源问题或者是系统内存脏页过多或者发生 swap 致使的。

大有界循环与 JIT 优化会给 SafePoint 带来哪些问题?

已知:只有线程执行到 Safepoint 代码才会知道Thread.intterupted()的最新状态 ,而不是线程的本地缓存。

咱们来看下面一段代码:

static int algorithm(int n) {
    int bestSoFar = 0;
    for (int i=0; i<n; ++i) {
        if (Thread.interrupted()) {
            System.out.println("broken by interrupted");
            break;
        }
        //增长pow计算,增长计算量,防止循环执行不超过1s就结束了
        bestSoFar = (int) Math.pow(i, 0.3);
    }
    return bestSoFar;
}
public static void main(String[] args) throws InterruptedException {
    Runnable task = () -> {
        Instant start = Instant.now();
        int bestSoFar = algorithm(1000000000);
        double durationInMillis = Duration.between(start, Instant.now()).toMillis();
        System.out.println("after "+durationInMillis+" ms, the result is "+bestSoFar);
    };
    
    //延迟1ms以后interrupt
    Thread t = new Thread(task);
    t.start();
    Thread.sleep(1);
    t.interrupt();
    
    //延迟10ms以后interrupt
    t = new Thread(task);
    t.start();
    Thread.sleep(10);
    t.interrupt();
    
    //延迟100ms以后interrupt
    t = new Thread(task);
    t.start();
    Thread.sleep(100);
    t.interrupt();
    
    //延迟1s以后interrupt
    //这时候 algorithm 里面的for循环调用次数应该足够了,会发生代码即时编译优化并 OSR
    t = new Thread(task);
    t.start();
    Thread.sleep(1000);
    //发现线程此次不会对 interrupt 有反应了
    t.interrupt();
}

以后利用 JVM 参数 -Xlog:jit+compilation=debug:file=jit_compile%t.log:uptime,level,tags:filecount=10,filesize=100M 打印 JIT 编译日志到另外一个文件,便于观察。最后控制台输出:

broken by interrupted
broken by interrupted
after 10.0 ms, the result is 27
after 1.0 ms, the result is 10
broken by interrupted
after 99.0 ms, the result is 69
after 29114.0 ms, the result is 501

能够看出,最后一次循环直接运行结束了,并无看到线程已经 interrupted 了。而且 JIT 编译日志能够看到,在最后一线程执行循环的时候发生了发生代码即时编译优化并 OSR:

[0.782s][debug][jit,compilation]  460 %     3       com.test.TypeTest::algorithm @ 4 (44 bytes)
[0.784s][debug][jit,compilation]  468       3       com.test.TypeTest::algorithm (44 bytes)
[0.794s][debug][jit,compilation]  486 %     4       com.test.TypeTest::algorithm @ 4 (44 bytes)
[0.797s][debug][jit,compilation]  460 %     3       com.test.TypeTest::algorithm @ 4 (44 bytes)   made not entrant
[0.799s][debug][jit,compilation]  503       4       com.test.TypeTest::algorithm (44 bytes)

3 还有 4 表示编译级别,% 表示是 OSR 栈上替换方法,也就是 for 循环还在执行的时候,进行了执行代码的机器码替换。在这以后,线程就看不到线程已经 interrupted 了,这说明,** JIT 优化后的代码,for 循环里面的 Safepoint 会被拿掉**。

这样带来的问题,也显而易见了,当须要 Stop the world 的时候,全部线程都会等着这个循环执行完,由于这个线程只有执行完这个大循环,才能进入 Safepoint。

那么,如何优化呢?

第一种方式是修改代码,将 for int 的循环变成 for long 类型:

for (long i=0; i<n; ++i) {
    if (Thread.interrupted()) {
        System.out.println("broken by interrupted");
        break;
    }
    //增长pow计算,增长计算量,防止循环执行不超过1s就结束了
    bestSoFar = (int) Math.pow(i, 0.3);
}

第二种是经过-XX:+UseCountedLoopSafepoints参数,让 JIT 优化代码的时候,不会拿掉有界循环里面的 SafePoint

用这两种方式其中一种以后的控制台输出:

broken by interrupted
broken by interrupted
after 0.0 ms, the result is 0
after 10.0 ms, the result is 29
broken by interrupted
after 100.0 ms, the result is 73
broken by interrupted
after 998.0 ms, the result is 170

如何经过日志分析 SafePoint?

目前,在 OpenJDK 11 版本,主要有两种 SafePoint 相关的日志。一种基本上只在开发时使用,另外一种能够在线上使用持续采集。

第一个是-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1,这个会定时采集,可是采集的时候会触发全部线程进入 Safepoint,因此,线程通常不打开(以前咱们对于定时让全部线程进入 Safepoint 都要关闭,这个就更不可能打开了)。而且,在 Java 12 中已经被移除,而且接下来的日志配置基本上能够替代这个,因此这里咱们就不赘述这个了。

另外是经过-Xlog:safepoint=trace:stdout:utctime,level,tags,对于 OpenJDK 的日志配置,能够参考个人另外一篇文章详细解析配置的格式,这里咱们直接用。

咱们这里配置了全部的 safepoint 相关的 JVM 日志都输出到控制台,一次 Stop the world 的时候,就会像下面这样输出:

[2020-07-14T07:08:26.197+0000][debug][safepoint] Safepoint synchronization initiated. (112 threads)
[2020-07-14T07:08:26.197+0000][info ][safepoint] Application time: 12.4565068 seconds
[2020-07-14T07:08:26.197+0000][trace][safepoint] Setting thread local yield flag for threads
[2020-07-14T07:08:26.197+0000][trace][safepoint] Thread: 0x0000022c7c494b30  [0x61dc] State: _at_safepoint _has_called_back 0 _at_poll_safepoint 0
[2020-07-14T07:08:26.197+0000][trace][safepoint] Thread: 0x0000022c7c497f30  [0x4ff8] State: _at_safepoint _has_called_back 0 _at_poll_safepoint 0
......省略一些处于 _at_poll_safepoint 的线程
[2020-07-14T07:08:26.197+0000][trace][safepoint] Thread: 0x0000022c10c010b0  [0x5878] State: _call_back _has_called_back 0 _at_poll_safepoint 0
[2020-07-14T07:08:26.348+0000][trace][safepoint] Thread: 0x0000022c10bfe560  [0x5038] State: _at_safepoint _has_called_back 0 _at_poll_safepoint 0
[2020-07-14T07:08:26.197+0000][debug][safepoint] Waiting for 1 thread(s) to block
[2020-07-14T07:08:29.348+0000][info ][safepoint] Entering safepoint region: G1CollectForAllocation
[2020-07-14T07:08:29.350+0000][info ][safepoint] Leaving safepoint region
[2020-07-14T07:08:29.350+0000][info ][safepoint] Total time for which application threads were stopped: 3.1499371 seconds, Stopping threads took: 3.1467255 seconds

首先,阶段 1 会打印日志,这个是 debug 级别的,表明要开始全局全部线程 Safepoint 了,这时候,JVM 就开始没法响应请求了,也就是 Stop the world 开始:

[2020-07-14T07:08:29.347+0000][debug][safepoint] Safepoint synchronization initiated. (112 threads)

阶段 2 不会打印日志,阶段 3 会打印:

[2020-07-14T07:08:26.197+0000][info ][safepoint] Application time: 12.4565068 seconds
[2020-07-14T07:08:26.197+0000][trace][safepoint] Setting thread local yield flag for threads
[2020-07-14T07:08:26.197+0000][trace][safepoint] Thread: 0x0000022c7c494b30  [0x61dc] State: _at_safepoint _has_called_back 0 _at_poll_safepoint 0
[2020-07-14T07:08:26.197+0000][trace][safepoint] Thread: 0x0000022c7c497f30  [0x4ff8] State: _at_safepoint _has_called_back 0 _at_poll_safepoint 0
......省略一些处于 _at_poll_safepoint 的线程
[2020-07-14T07:08:26.197+0000][trace][safepoint] Thread: 0x0000022c10c010b0  [0x5878] State: _call_back _has_called_back 0 _at_poll_safepoint 0
[2020-07-14T07:08:26.348+0000][trace][safepoint] Thread: 0x0000022c10bfe560  [0x5038] State: _at_safepoint _has_called_back 0 _at_poll_safepoint 0
[2020-07-14T07:08:26.197+0000][debug][safepoint] Waiting for 1 thread(s) to block

Application time: 12.4565068 seconds 表明上次全局 Safepoint 与此次 Safepoint 间隔了多长时间。后面 trace 的日志表示每一个线程的状态,其中没有处于 Safepoint 的只有一个:

Thread: 0x0000022c10c010b0  [0x5878] State: _call_back _has_called_back 0 _at_poll_safepoint 0

这里有详细的线程号,能够经过 jstack 知道这个线程是干啥的。

最后的Waiting for 1 thread(s) to block也表明到底须要等待几个线程走到 Safepoint。

阶段 4 执行完,开始阶段 5 的时候,会打印:

 

[2020-07-14T07:08:29.348+0000][info ][safepoint] Entering safepoint region: G1CollectForAllocation

阶段 5 执行完以后,会打印:

[2020-07-14T07:08:29.350+0000][info ][safepoint] Leaving safepoint region

最后阶段 6 开始的时候,会打印:

[2020-07-14T07:08:29.350+0000][info ][safepoint] Total time for which application threads were stopped: 3.1499371 seconds, Stopping threads took: 3.1467255 seconds

Total time for which application threads were stopped是此次阶段1到阶段6开始,一共过了多长时间,也就是 Stop the world 多长时间。后面的Stopping threads took是此次等待线程走进 Safepoint 过了多长时间,通常除了 阶段 5 执行触发 Stop the world 之外,都是因为 等待线程走进 Safepoint 时间长。这是就要看 trace 的线程哪些没有处于 Safepoint,看他们干了什么,是否有大循环,或者是使用了StackWalker这个类.

如何经过 JFR 分析 SafePoint?

JFR 相关的配置以及说明以及如何经过 JFR 分析 SafePoint 相关事件,能够参考个人另外一个系列JFR全解系列

常见的 SafePoint 调优参数以及讲解

建议关闭定时让全部线程进入 Safepoint

对于微服务高并发应用,不必定时进入 Safepoint,因此关闭 -XX:+UnlockDiagnosticVMOptions -XX:GuaranteedSafepointInterval=0

建议取消偏向锁

在高并发应用中,偏向锁并不能带来性能提高,反而由于偏向锁取消带来了不少不必的某些线程进入Safepoint 或者 Stop the world。因此建议关闭:-XX:-UseBiasedLocking

建议打开循环内添加 Safepoint 参数

防止大循环 JIT 编译致使内部 Safepoint 被优化省略,致使进入 SafePoint 时间变长:-XX:+UseCountedLoopSafepoints

建议打开 debug 级别的 safepoint 日志(和第五个选一个)

debug 级别虽然看不到每次是哪些线程须要等待进入 Safepoint,可是总体每阶段耗时已经很清楚了。若是是 trace 级别,每次都能看到是那些线程,可是这样每次进入 safepoint 时间就会增长几毫秒。

-Xlog:safepoint=debug:file=safepoint.log:utctime,level,tags:filecount=50,filesize=100M

建议打开 JFR 关于 safepoint 的采集(和第四个选一个)

修改,或者新建 jfc 文件:

<event name="jdk.SafepointBegin">
  <setting name="enabled">true</setting>
  <setting name="threshold">10 ms</setting>
</event>

<event name="jdk.SafepointStateSynchronization">
  <setting name="enabled">true</setting>
  <setting name="threshold">10 ms</setting>
</event>

<event name="jdk.SafepointWaitBlocked">
  <setting name="enabled">true</setting>
  <setting name="threshold">10 ms</setting>
</event>

<event name="jdk.SafepointCleanup">
  <setting name="enabled">true</setting>
  <setting name="threshold">10 ms</setting>
</event>

<event name="jdk.SafepointCleanupTask">
  <setting name="enabled">true</setting>
  <setting name="threshold">10 ms</setting>
</event>

<event name="jdk.SafepointEnd">
  <setting name="enabled">true</setting>
  <setting name="threshold">10 ms</setting>
</event>

<event name="jdk.ExecuteVMOperation">
  <setting name="enabled">true</setting>
  <setting name="threshold">10 ms</setting>
</event>

看完三件事❤️

若是你以为这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有大家的 『点赞和评论』,才是我创造的动力。

  2. 关注公众号 『 java烂猪皮 』,不按期分享原创知识。

  3. 同时能够期待后续文章ing🚀



做者:张哈希

出处:https://club.perfma.com/article/2132225

相关文章
相关标签/搜索