前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >2024-4-10 群讨论:JFR 热点方法采样实现原理

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

作者头像
干货满满张哈希
发布2024-05-25 09:02:08
1010
发布2024-05-25 09:02:08
举报
文章被收录于专栏:干货满满张哈希

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

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

57929a0c3ccd34a8ea2b041a603f5051.jpeg
57929a0c3ccd34a8ea2b041a603f5051.jpeg
c108e30ed2e9e20f6ce3641b807f51b3.jpeg
c108e30ed2e9e20f6ce3641b807f51b3.jpeg
76c70305c49e63f72941d5e2ee2b128b.jpeg
76c70305c49e63f72941d5e2ee2b128b.jpeg

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

<event&nbsp;name="jdk.ExecutionSample"> &nbsp;&nbsp;&nbsp;&nbsp;<setting&nbsp;name="enabled"&nbsp;control="method-sampling-enabled">true</setting> &nbsp;&nbsp;&nbsp;&nbsp;<setting&nbsp;name="period"&nbsp;control="method-sampling-java-interval">20 ms</setting> </event> <event&nbsp;name="jdk.NativeMethodSample"> &nbsp;&nbsp;&nbsp;&nbsp;<setting&nbsp;name="enabled"&nbsp;control="method-sampling-enabled">true</setting> &nbsp;&nbsp;&nbsp;&nbsp;<setting&nbsp;name="period"&nbsp;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&nbsp;JfrThreadSampler::run()&nbsp;{ &nbsp;&nbsp;assert(_sampler_thread ==&nbsp;nullptr,&nbsp;"invariant"); &nbsp;&nbsp;_sampler_thread =&nbsp;this; &nbsp;&nbsp;//获取上次 java 方法采样时间与原生方法采样时间 &nbsp;&nbsp;int64_t&nbsp;last_java_ms =&nbsp;get_monotonic_ms(); &nbsp;&nbsp;int64_t&nbsp;last_native_ms = last_java_ms; &nbsp;&nbsp;//然后,在一个死循环中,不断的等待采样间隔到达,然后对应采样 &nbsp;&nbsp;while&nbsp;(true) { &nbsp;&nbsp;&nbsp;&nbsp;//省略等待采样间隔(就是上面的 20ms 配置)的代码 &nbsp;&nbsp;&nbsp;&nbsp;//采样 java 方法 &nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(next_j <= sleep_to_next) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;task_stacktrace(JAVA_SAMPLE, &_last_thread_java); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;last_java_ms =&nbsp;get_monotonic_ms(); &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;//采样原生方法 &nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(next_n <= sleep_to_next) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;task_stacktrace(NATIVE_SAMPLE, &_last_thread_native); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;last_native_ms =&nbsp;get_monotonic_ms(); &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;} }

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

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

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

bool&nbsp;JfrThreadSampleClosure::do_sample_thread(JavaThread* thread, JfrStackFrame* frames, u4 max_frames, JfrSampleType type)&nbsp;{ &nbsp;&nbsp;assert(Threads_lock->owned_by_self(),&nbsp;"Holding the thread table lock."); &nbsp;&nbsp;//判断线程是否是被排除的,一般 VM 线程是被排除的 &nbsp;&nbsp;if&nbsp;(is_excluded(thread)) { &nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;false; &nbsp;&nbsp;} &nbsp;&nbsp;bool&nbsp;ret =&nbsp;false; &nbsp;&nbsp;//设置线程的 trace flag &nbsp;&nbsp;thread->set_trace_flag();&nbsp;&nbsp; &nbsp;&nbsp;//保证线程 trace flag 可见性,仅针对 UseSystemMemoryBarrier 为 true 的情况,默认是 false &nbsp;&nbsp;if&nbsp;(UseSystemMemoryBarrier) { &nbsp;&nbsp;&nbsp;&nbsp;SystemMemoryBarrier::emit(); &nbsp;&nbsp;} &nbsp;&nbsp; &nbsp;&nbsp;if&nbsp;(JAVA_SAMPLE == type) { &nbsp;&nbsp;&nbsp;&nbsp;//判断线程是否是处于 RUNNABLE 或者 RUNNING 并且是在运行 java 代码的状态 &nbsp;&nbsp;&nbsp;&nbsp;//如果是,则采样 &nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(thread_state_in_java(thread)) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ret =&nbsp;sample_thread_in_java(thread, frames, max_frames); &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;}&nbsp;else&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;assert(NATIVE_SAMPLE == type,&nbsp;"invariant"); &nbsp;&nbsp;&nbsp;&nbsp;//判断线程是否是处于 RUNNABLE 或者 RUNNING 并且是在运行原生代码的状态 &nbsp;&nbsp;&nbsp;&nbsp;//如果是,则采样 &nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(thread_state_in_native(thread)) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ret =&nbsp;sample_thread_in_native(thread, frames, max_frames); &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;} &nbsp;&nbsp;clear_transition_block(thread); &nbsp;&nbsp;return&nbsp;ret; }

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

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

与 async-profiler 的应用场景对比

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

其中这个火焰图:

40296ff516636603ad761cdd4d785723.jpeg
40296ff516636603ad761cdd4d785723.jpeg

就是 JFR 的&nbsp;jdk.ExecutionSample&nbsp;和&nbsp;jdk.NativeMethodSample&nbsp;事件结合了&nbsp;jdk.ContainerCPUUsage&nbsp;和&nbsp;jdk.ThreadCPULoad&nbsp;事件构建的火焰图。

async profiler 的采样方式,和 JFR 的不同。JFR 的是尽量保持低消耗,但是对于 Java 方法一次采样对于运行 Java 代码的最多 5 个线程,对于 Native 的最多 1 个,但是全局基本不加锁,也不加安全点导致全局暂停,所以消耗很低,并且一般足以定位高 CPU 消耗瓶颈问题(参考上面我发的定位一个实际问题的链接)。async profiler 的采样方式,对于原生方法更详细,对于 Java 方法一般需要 JVM 启动的时候打开&nbsp;-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints,否则只能采集到 Java 安全点时候的方法。因为默认 JVM 为了提高性能,只在安全点的时候添加 Debug 信息用于定位问题带上方法调用信息,加上前面的&nbsp;-XX:+DebugNonSafepoints&nbsp;会去掉限制,在所有位置加上 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 生态里面的官方英文文档以及中文文档作为语料,按照内容段交叉拆分,进行微调,用于字幕翻译。。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-05-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是 JFR 热点方法采样,效果是什么样子?
  • 采样的原理是?
  • 与 async-profiler 的应用场景对比
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档