以下来自本人拉的一个关于 Java 技术的讨论群。关注公众号:hashcon,私信拉你
什么是 JFR 热点方法采样,效果是什么样子?
其实对应的就是 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 采样的原理就是:
- 一个固定的线程,不断的等待采样间隔到达,然后对应采样
- 采样的时候,遍历所有线程,判断线程是否符合采样条件,符合则采样
- 采样的时候,对于 java 方法采样,会采样最多 5 个线程的 java 方法,对于原生方法采样,会采样最多 1 个线程的原生方法
- 采样的时候,判断线程是否符合采样条件,主要是判断线程是否是处于 RUNNABLE 或者 RUNNING 并且是在运行 java 代码或者原生代码的状态
与 async-profiler 的应用场景对比
这两个 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,目前成果是微调了三个模型:
- 一个模型是基于 whisper 模型的微调,使用我原来做的精翻的视频按照语句段落切分的片段,并尝试按照方言类别,以及技术类别分别尝试微调的成果。用于视频字幕识别。
- 一个模型是基于 Mistral Large 的模型的微调,识别提取视频课件的片段,辅以实际的课件文字进行识别微调。用于识别课件的片段。
- 最后一个模型是基于 Claude 3 的模型微调,使用我之前制作的翻译字幕,与 AWS、Go 社区、CNCF 生态里面的官方英文文档以及中文文档作为语料,按照内容段交叉拆分,进行微调,用于字幕翻译。
目前,准确率已经非常高了。大家如果有想要我制作的视频,欢迎关注留言。
本人也是开源代码爱好者,贡献过很多项目的源码(Mycat 和 Java JFRUnit 的核心贡献者,贡献过 OpenJDK,Spring,Spring Cloud,Apache Bookkeeper,Apache RocketMQ,Ribbon,Lettuce、 SocketIO、Longchain4j 等项目 ),同时也是深度技术迷,编写过很多硬核的原理分析系列(JVM)。本人也有一个 Java 技术交流群,感兴趣的欢迎关注。
另外,一如即往的是,全网的所有收益,都会捐赠给希望工程,坚持靠爱与兴趣发电。