其实对应的就是 jdk.ExecutionSample 和 jdk.NativeMethodSample 事件
这两个事件是用来采样的,采样的频率是可以配置的,默认配置在: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 采样的原理就是:
这两个 JFR 时间一般用于构建 JFR 火焰图,我之前定位代码高 CPU 消耗瓶颈很多是通过这个定位,有一个例子是:https://juejin.cn/post/7325623087209742374
其中这个火焰图:
就是 JFR 的 jdk.ExecutionSample 和 jdk.NativeMethodSample 事件结合了 jdk.ContainerCPUUsage 和 jdk.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,目前成果是微调了三个模型: