掘金 后端 ( ) • 2024-04-12 22:08

以下来自本人拉的一个关于 Java 技术的讨论群。关注公众号:hashcon,私信拉你

什么是 JFR 热点方法采样,效果是什么样子?

其实对应的就是 jdk.ExecutionSamplejdk.NativeMethodSample 事件

01.png

02.png

03.png

这两个事件是用来采样的,采样的频率是可以配置的,默认配置在:default.jfc(https://github.com/openjdk/jdk/blob/master/src/jdk.jfr/share/conf/jfr/default.jfc):

<event name="jdk.ExecutionSample">
    <setting name="enabled" control="method-sampling-enabled">true</setting>
    <setting name="period" control="method-sampling-java-interval">20 ms</setting>
</event>

<event name="jdk.NativeMethodSample">
    <setting name="enabled" control="method-sampling-enabled">true</setting>
    <setting name="period" control="method-sampling-native-interval">20 ms</setting>
</event>

默认都是启用的,都是 20ms 一次。这个听上去消耗很大,实际上消耗很小的,详见下一节原理。

采样的原理是?

一切从源码出发https://github.com/openjdk/jdk/blob/master/src/hotspot/share/jfr/periodic/sampling/jfrThreadSampler.cpp

//固定开启一个线程,用于 jfr java 方法与原生方法采样
void JfrThreadSampler::run() {
  assert(_sampler_thread == nullptr, "invariant");

  _sampler_thread = this;
  //获取上次 java 方法采样时间与原生方法采样时间
  int64_t last_java_ms = get_monotonic_ms();
  int64_t last_native_ms = last_java_ms;
  //然后,在一个死循环中,不断的等待采样间隔到达,然后对应采样
  while (true) {
    //省略等待采样间隔(就是上面的 20ms 配置)的代码
    //采样 java 方法
    if (next_j <= sleep_to_next) {
      task_stacktrace(JAVA_SAMPLE, &_last_thread_java);
      last_java_ms = get_monotonic_ms();
    }
    //采样原生方法
    if (next_n <= sleep_to_next) {
      task_stacktrace(NATIVE_SAMPLE, &_last_thread_native);
      last_native_ms = get_monotonic_ms();
    }
  }
}

采样原生方法和 java 方法的代码是一样的,都是调用 task_stacktrace 方法,这个方法的实现:

static const uint MAX_NR_OF_JAVA_SAMPLES = 5;
static const uint MAX_NR_OF_NATIVE_SAMPLES = 1;

void JfrThreadSampler::task_stacktrace(JfrSampleType type, JavaThread** last_thread) {
  ResourceMark rm;
  //对于 java 方法采样,会采样 MAX_NR_OF_JAVA_SAMPLES 即 5 个线程的 java 方法
  EventExecutionSample samples[MAX_NR_OF_JAVA_SAMPLES];
  //对于原生方法采样,会采样 MAX_NR_OF_NATIVE_SAMPLES 即 1 个线程的原生方法
  EventNativeMethodSample samples_native[MAX_NR_OF_NATIVE_SAMPLES];
  JfrThreadSampleClosure sample_task(samples, samples_native);

  const uint sample_limit = JAVA_SAMPLE == type ? MAX_NR_OF_JAVA_SAMPLES : MAX_NR_OF_NATIVE_SAMPLES;
  uint num_samples = 0;
  JavaThread* start = nullptr;
  {
    elapsedTimer sample_time;
    sample_time.start();
    {
      //获取所有线程列表
      MutexLocker tlock(Threads_lock);
      ThreadsListHandle tlh;
      JavaThread* current = _cur_index != -1 ? *last_thread : nullptr;
      const JfrBuffer* enqueue_buffer = get_enqueue_buffer();
      assert(enqueue_buffer != nullptr, "invariant");
      //然后,遍历线程,收集采样数据,直到达到前面提到的 MAX_NR_OF_JAVA_SAMPLES 或 MAX_NR_OF_NATIVE_SAMPLES
      while (num_samples < sample_limit) {
        current = next_thread(tlh.list(), start, current);
        if (current == nullptr) {
          break;
        }
        if (start == nullptr) {
          start = current;  // remember the thread where we started to attempt sampling
        }
        if (current->is_Compiler_thread()) {
          continue;
        }
        assert(enqueue_buffer->free_size() >= _min_size, "invariant");
        //判断线程状态是否是符合采样的,并采样
        if (sample_task.do_sample_thread(current, _frames, _max_frames, type)) {
          num_samples++;
        }
        enqueue_buffer = renew_if_full(enqueue_buffer);
      }
      *last_thread = current;  // remember the thread we last attempted to sample
    }
    sample_time.stop();
    log_trace(jfr)("JFR thread sampling done in %3.7f secs with %d java %d native samples",
                   sample_time.seconds(), sample_task.java_entries(), sample_task.native_entries());
  }
  if (num_samples > 0) {
    sample_task.commit_events(type);
  }
}

如何判断线程是否符合采样并采样的呢?这个是在 sample_task.do_sample_thread 方法中判断的,这个方法的实现:

bool JfrThreadSampleClosure::do_sample_thread(JavaThread* thread, JfrStackFrame* frames, u4 max_frames, JfrSampleType type) {
  assert(Threads_lock->owned_by_self(), "Holding the thread table lock.");
  //判断线程是否是被排除的,一般 VM 线程是被排除的
  if (is_excluded(thread)) {
    return false;
  }

  bool ret = false;
  //设置线程的 trace flag
  thread->set_trace_flag();  
  //保证线程 trace flag 可见性,仅针对 UseSystemMemoryBarrier 为 true 的情况,默认是 false
  if (UseSystemMemoryBarrier) {
    SystemMemoryBarrier::emit();
  }
  
  if (JAVA_SAMPLE == type) {
    //判断线程是否是处于 RUNNABLE 或者 RUNNING 并且是在运行 java 代码的状态
    //如果是,则采样
    if (thread_state_in_java(thread)) {
      ret = sample_thread_in_java(thread, frames, max_frames);
    }
  } else {
    assert(NATIVE_SAMPLE == type, "invariant");
    //判断线程是否是处于 RUNNABLE 或者 RUNNING 并且是在运行原生代码的状态
    //如果是,则采样
    if (thread_state_in_native(thread)) {
      ret = sample_thread_in_native(thread, frames, max_frames);
    }
  }
  clear_transition_block(thread);
  return ret;
}

总结看来,JFR 采样的原理就是:

  1. 一个固定的线程,不断的等待采样间隔到达,然后对应采样
  2. 采样的时候,遍历所有线程,判断线程是否符合采样条件,符合则采样
  3. 采样的时候,对于 java 方法采样,会采样最多 5 个线程的 java 方法,对于原生方法采样,会采样最多 1 个线程的原生方法
  4. 采样的时候,判断线程是否符合采样条件,主要是判断线程是否是处于 RUNNABLE 或者 RUNNING 并且是在运行 java 代码或者原生代码的状态

与 async-profiler 的应用场景对比

这两个 JFR 时间一般用于构建 JFR 火焰图,我之前定位代码高 CPU 消耗瓶颈很多是通过这个定位,有一个例子是:https://juejin.cn/post/7325623087209742374

其中这个火焰图:

img.png

就是 JFR 的 jdk.ExecutionSamplejdk.NativeMethodSample 事件结合了 jdk.ContainerCPUUsagejdk.ThreadCPULoad 事件构建的火焰图。

async profiler 的采样方式,和 JFR 的不同。JFR 的是尽量保持低消耗,但是对于 Java 方法一次采样对于运行 Java 代码的最多 5 个线程,对于 Native 的最多 1 个,但是全局基本不加锁,也不加安全点导致全局暂停,所以消耗很低,并且一般足以定位高 CPU 消耗瓶颈问题(参考上面我发的定位一个实际问题的链接)。async profiler 的采样方式,对于原生方法更详细,对于 Java 方法一般需要 JVM 启动的时候打开 -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints,否则只能采集到 Java 安全点时候的方法。因为默认 JVM 为了提高性能,只在安全点的时候添加 Debug 信息用于定位问题带上方法调用信息,加上前面的 -XX:+DebugNonSafepoints 会去掉限制,在所有位置加上 Debug 信息以及日志记录,这样 async profiler 才能采集到详细的 Java 方法调用信息。所以整体上 async profiler 的采样方式更详细,但是消耗也更大。

建议是,长期开着 JFR,遇到问题优先回溯 JFR,如果 JFR 无法定位问题,再使用 async profiler。

个人简介:个人业余研究了 AI LLM 微调与 RAG,目前成果是微调了三个模型:

  1. 一个模型是基于 whisper 模型的微调,使用我原来做的精翻的视频按照语句段落切分的片段,并尝试按照方言类别,以及技术类别分别尝试微调的成果。用于视频字幕识别。
  2. 一个模型是基于 Mistral Large 的模型的微调,识别提取视频课件的片段,辅以实际的课件文字进行识别微调。用于识别课件的片段。
  3. 最后一个模型是基于 Claude 3 的模型微调,使用我之前制作的翻译字幕,与 AWS、Go 社区、CNCF 生态里面的官方英文文档以及中文文档作为语料,按照内容段交叉拆分,进行微调,用于字幕翻译。

目前,准确率已经非常高了。大家如果有想要我制作的视频,欢迎关注留言。

本人也是开源代码爱好者,贡献过很多项目的源码(Mycat 和 Java JFRUnit 的核心贡献者,贡献过 OpenJDK,Spring,Spring Cloud,Apache Bookkeeper,Apache RocketMQ,Ribbon,Lettuce、 SocketIO、Longchain4j 等项目 ),同时也是深度技术迷,编写过很多硬核的原理分析系列(JVM)。本人也有一个 Java 技术交流群,感兴趣的欢迎关注。

另外,一如即往的是,全网的所有收益,都会捐赠给希望工程,坚持靠爱与兴趣发电。