2024-4-10 群讨论:JFR 热点方法采样实现原理

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

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

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

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

xml 复制代码
<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 一次。这个听上去消耗很大,实际上消耗很小的,详见下一节原理。

采样的原理是?

一切从源码出发github.com/openjdk/jdk...

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 方法,这个方法的实现:

cpp 复制代码
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 方法中判断的,这个方法的实现:

cpp 复制代码
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 消耗瓶颈很多是通过这个定位,有一个例子是:juejin.cn/post/732562...

其中这个火焰图:

就是 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 技术交流群,感兴趣的欢迎关注。
    另外,一如即往的是,全网的所有收益,都会捐赠给希望工程,坚持靠爱与兴趣发电。
相关推荐
蓝澈112120 分钟前
迪杰斯特拉算法之解决单源最短路径问题
java·数据结构
Kali_0727 分钟前
使用 Mathematical_Expression 从零开始实现数学题目的作答小游戏【可复制代码】
java·人工智能·免费
rzl0239 分钟前
java web5(黑马)
java·开发语言·前端
时序数据说42 分钟前
为什么时序数据库IoTDB选择Java作为开发语言
java·大数据·开发语言·数据库·物联网·时序数据库·iotdb
君爱学习44 分钟前
RocketMQ延迟消息是如何实现的?
后端
guojl1 小时前
深度解读jdk8 HashMap设计与源码
java
Falling421 小时前
使用 CNB 构建并部署maven项目
后端
guojl1 小时前
深度解读jdk8 ConcurrentHashMap设计与源码
java
程序员小假1 小时前
我们来讲一讲 ConcurrentHashMap
后端