深入剖析理解AsyncGetCallTrace源码底层原理

目录
  • 前言
  • 源码实现
    • 核心数据结构
    • 函数申明
    • AsyncGetCallTrace 实现
    • pd_get_top_frame_for_signal_handler 实现
    • pd_get_top_frame 实现
    • forte_fill_call_trace_given_top 实现
  • 总结

前言

AsyncGetCallTrace 是由 OracleJDK/OpenJDK 内部提供的一个函数,该函数可以在 JVM 未进入 safepoint 时正常获取到当前线程的调用栈(换句话说,使用该函数获取线程栈时,不会要求 JVM 进入 safepoint。而进入 safepoint 对于 OpenJDK或者 OracleJDK 来说意味着会 STW 的发生,所以这意味着使用该函数获取线程栈不会产生 STW,It’s amazing.)。目前该函数仅在 Linux X86、Solaris SPARC、Solaris X86 系统下支持。
另外它还支持在 UNIX 信号处理器中被异步调用,那么我们只需注册一个 UNIX 信号处理器,并在Handler中调用 AsyncGetCallTrace 获取当前线程的调用栈即可。由于 UNIX 信号会被随机的发送给进程的某一线程进行处理,因此可以认为获取所有线程的调用栈样本是均匀的。
但是值得注意的是,该函数不是标准的 JVM API,所以使用的时候,可能存在以下问题:

移植性问题,因为只能跑在 OpenJDK 或者 OracleJDK 上

由于不是标准的 JVMTI API,所以使用者需要通过特殊一些方式来获取该函数,这给使用者带来了一些不便,但是这也无大碍。

源码实现

关于怎么使用该函数去进行热点方法采样的方法,不在本节的讨论范围,在参考资料中,有一些描述,如果还有不清楚的,也可以给我留言交流。

核心数据结构

// call frame copied from old .h file and renamed
//  Fields:
//    1) For Java frame (interpreted and compiled),
//       lineno    - bci of the method being executed or -1 if bci is not available
//       method_id - jmethodID of the method being executed
//    2) For native method
//       lineno    - (-3)
//       method_id - jmethodID of the method being executed
typedef struct {
    jint lineno;                      // numberline number in the source file
    jmethodID method_id;              // method executed in this frame
} ASGCT_CallFrame;
// call trace copied from old .h file and renamed
// Fields:
//   env_id     - ID of thread which executed this trace.
//   num_frames - number of frames in the trace.
//                (< 0 indicates the frame is not walkable).
//   frames     - the ASGCT_CallFrames that make up this trace. Callee followed by callers.
typedef struct {
    JNIEnv *env_id;                   // Env where trace was recorded
    jint num_frames;                  // number of frames in this trace
    ASGCT_CallFrame *frames;          // frames
} ASGCT_CallTrace;
// These name match the names reported by the forte quality kit
// 这些枚举是对应到 ASGCT_CallTrace 中的 num_frames 的返回值的
// 举个例子,当 JVM 在进行 GC 时,返回值中
// ASGCT_CallTrace.num_frames == ticks_GC_active
enum {
  ticks_no_Java_frame         =  0,
  ticks_no_class_load         = -1,
  ticks_GC_active             = -2,
  ticks_unknown_not_Java      = -3,
  ticks_not_walkable_not_Java = -4,
  ticks_unknown_Java          = -5,
  ticks_not_walkable_Java     = -6,
  ticks_unknown_state         = -7,
  ticks_thread_exit           = -8,
  ticks_deopt                 = -9,
  ticks_safepoint             = -10
};

函数申明

//   trace    - trace data structure to be filled by the VM.
//   depth    - depth of the call stack trace.
//   ucontext - ucontext_t of the LWP
void AsyncGetCallTrace(ASGCT_CallTrace *trace, jint depth, void* ucontext)

该函数的调用者获取到的栈是属于某一个特定线程的,这个线程是由trace->env_id 唯一标识的,而且 该标识的线程 必须和当前执行 AsyncGetCallTrace 方法的 线程 是同一线程。同时调用者需要为 trace->frames 分配足够多的内存,来保存栈深最多为 depth 的栈。若获取到了有关的栈,JVM 会自动把相关的堆栈信息写入 trace 中。接下来我们通过源码来看看内部实现。

AsyncGetCallTrace 实现

具体分析直接看代码里面的注释,为了保持完整性,该方法的任何代码我都没删除,源码位置:/hotspot/src/share/vm/prims/forte.cpp

void AsyncGetCallTrace(ASGCT_CallTrace *trace, jint depth, void* ucontext) {
  JavaThread* thread;
  // 1. 首先判断 jniEnv 是否为空,或者 jniEnv 对应的线程是否有效,或者该线程是否已经退出,
  // 任一条件满足,则返回 ticks_thread_exit(对应为核心数据结构中枚举类型所示)
  if (trace->env_id == NULL ||
    (thread = JavaThread::thread_from_jni_environment(trace->env_id)) == NULL ||
    thread->is_exiting()) {
    // bad env_id, thread has exited or thread is exiting
    trace->num_frames = ticks_thread_exit; // -8
    return;
  }
  if (thread->in_deopt_handler()) {
    // thread is in the deoptimization handler so return no frames
    trace->num_frames = ticks_deopt; // -9
    return;
  }
  // 2. 这里对 jniEnv 所指线程是否是当前线程进行断言,如果不相等则直接报错
  assert(JavaThread::current() == thread,
         "AsyncGetCallTrace must be called by the current interrupted thread");
  // 3. JVMTI_EVENT_CLASS_LOAD 事件必须 enable,否则直接返回 ticks_no_class_load
  if (!JvmtiExport::should_post_class_load()) {
    trace->num_frames = ticks_no_class_load; // -1
    return;
  }
  // 4. 当前 heap 必须没有进行 GC ,否则直接返回 ticks_GC_active
  if (Universe::heap()->is_gc_active()) {
    trace->num_frames = ticks_GC_active; // -2
    return;
  }
  // 5. 根据线程的当前状态来获取对应的线程栈,只有在线程的状态为 _thread_in_vm/_thread_in_vm_trans
  // 和 _thread_in_Java/_thread_in_Java_trans 时才会进行线程栈的爬取
  switch (thread->thread_state()) {
  case _thread_new:
  case _thread_uninitialized:
  case _thread_new_trans:
    // We found the thread on the threads list above, but it is too
    // young to be useful so return that there are no Java frames.
    trace->num_frames = 0;
    break;
  case _thread_in_native:
  case _thread_in_native_trans:
  case _thread_blocked:
  case _thread_blocked_trans:
  case _thread_in_vm:
  case _thread_in_vm_trans:
    {
      frame fr;
      // 首先获取当前线程的栈顶栈帧
      // param isInJava == false - indicate we aren't in Java code
      if (!thread->pd_get_top_frame_for_signal_handler(&fr, ucontext, false)) {
        trace->num_frames = ticks_unknown_not_Java;  // -3 unknown frame
      } else {
        //  该线程如果没有任何的 Java 栈帧,直接返回 0 帧
        if (!thread->has_last_Java_frame()) {
          trace->num_frames = 0; // No Java frames
        } else {
          trace->num_frames = ticks_not_walkable_not_Java;    // -4 non walkable frame by default

          // 如果存在合法的栈帧,则填充 trace 中的 frames 和 num_frames
          forte_fill_call_trace_given_top(thread, trace, depth, fr);
          ...
        }
      }
    }
    break;
  case _thread_in_Java:
  case _thread_in_Java_trans:
    {
      frame fr;
      // param isInJava == true - indicate we are in Java code
      if (!thread->pd_get_top_frame_for_signal_handler(&fr, ucontext, true)) {
        trace->num_frames = ticks_unknown_Java;  // -5 unknown frame
      } else {
        trace->num_frames = ticks_not_walkable_Java;  // -6, non walkable frame by default
        forte_fill_call_trace_given_top(thread, trace, depth, fr);
      }
    }
    break;
  default:
    // Unknown thread state
    trace->num_frames = ticks_unknown_state; // -7
    break;
  }
}

从以上的分析中,最终获取线程栈的地方主要在这两个方法 pd_get_top_frame_for_signal_handler 和 forte_fill_call_trace_given_top,接下来我们来看下这两个方法的实现。

pd_get_top_frame_for_signal_handler 实现

从方法名不难看出,该方法的主要作用是获取当前线程的栈顶帧。后面跟了个 signal_handler,最初的想法我猜应该是为响应 UNIX 下的 SIGPROF 信号的。因为本身 AsyncGetCallTrace 就是为此而生的。该方法的源码位置 /hotspot/src/os_cpu/linux_x86/vm/thread_linux_x86.cpp

bool JavaThread::pd_get_top_frame_for_signal_handler(frame* fr_addr,
  void* ucontext, bool isInJava) {
  assert(Thread::current() == this, "caller must be current thread");
  return pd_get_top_frame(fr_addr, ucontext, isInJava);
}

很简单,判断一下是否是当前线程,至于 isInJava 入参是和当前的线程的状态相关的,如果是跑在 java 代码内,则为 true,否则为 false。

pd_get_top_frame 实现

bool JavaThread::pd_get_top_frame(frame* fr_addr, void* ucontext, bool isInJava) {
  assert(this->is_Java_thread(), "must be JavaThread");
  JavaThread* jt = (JavaThread *)this;
  // If we have a last_Java_frame, then we should use it even if
  // isInJava == true.  It should be more reliable than ucontext info.
  if (jt->has_last_Java_frame() && jt->frame_anchor()->walkable()) {
    *fr_addr = jt->pd_last_frame();
    return true;
  }
  // At this point, we don't have a last_Java_frame, so
  // we try to glean some information out of the ucontext
  // if we were running Java code when SIGPROF came in.
  if (isInJava) {
    ucontext_t* uc = (ucontext_t*) ucontext;
    intptr_t* ret_fp;
    intptr_t* ret_sp;
    ExtendedPC addr = os::Linux::fetch_frame_from_ucontext(this, uc,
      &ret_sp, &ret_fp);
    if (addr.pc() == NULL || ret_sp == NULL ) {
      // ucontext wasn't useful
      return false;
    }
    frame ret_frame(ret_sp, ret_fp, addr.pc());
    if (!ret_frame.safe_for_sender(jt)) {
#ifdef COMPILER2
      // C2 uses ebp as a general register see if NULL fp helps
      frame ret_frame2(ret_sp, NULL, addr.pc());
      if (!ret_frame2.safe_for_sender(jt)) {
        // nothing else to try if the frame isn't good
        return false;
      }
      ret_frame = ret_frame2;
#else
      // nothing else to try if the frame isn't good
      return false;
#endif /* COMPILER2 */
    }
    *fr_addr = ret_frame;
    return true;
  }
  // nothing else to try
  return false;
}

实际上拿栈顶帧的函数,由于函数的源码较长,我就简短的描述一下逻辑

  • 当前线程只能是 java 线程
  • 判断栈顶帧是否存在,并且当前的栈是 walkable 的,若二者的满足,则返回 javaThread 的 pd_last_frame,即栈顶帧,结束;否则继续;
  • 如果当前线程是跑 java 代码,那么我们尝试在 ucontext_t 内收集一些我们需要的信息,比如说栈帧

forte_fill_call_trace_given_top 实现

当我们获取到栈顶的帧之后,接下来的事情就顺理成章了,只要从栈顶开始,遍历整个堆栈就能把所有的方法都获取到了,同时将获取到的结果保存到ASGCT_CallTrace,源码位置:/hotspot/src/share/vm/prims/forte.cpp

static void forte_fill_call_trace_given_top(JavaThread* thd,
                                            ASGCT_CallTrace* trace,
                                            int depth,
                                            frame top_frame) {
  NoHandleMark nhm;
  frame initial_Java_frame;
  Method* method;
  int bci = -1; // assume BCI is not available for method
                // update with correct information if available
  int count;
  count = 0;
  assert(trace->frames != NULL, "trace->frames must be non-NULL");
  // 1. 获取到栈顶的第一个 java 栈帧
  // Walk the stack starting from 'top_frame' and search for an initial Java frame.
  find_initial_Java_frame(thd, &top_frame, &initial_Java_frame, &method, &bci);
  // Check if a Java Method has been found.
  if (method == NULL) return;
  //  2. 如果不是合法的方法,直接返回
  if (!method->is_valid_method()) {
    trace->num_frames = ticks_GC_active; // -2
    return;
  }
  vframeStreamForte st(thd, initial_Java_frame, false);
  // 循环迭代栈上的所有栈帧,一一获取每个方法 bci 和 方法 id,这里会用上从外面传入的最大栈深 depth
  for (; !st.at_end() && count < depth; st.forte_next(), count++) {
    bci = st.bci();
    method = st.method();
    if (!method->is_valid_method()) {
      // we throw away everything we've gathered in this sample since
      // none of it is safe
      trace->num_frames = ticks_GC_active; // -2
      return;
    }
    // 根据方法对象获取方法 id,如果方法 id 在此时还未产生,则返回 NULL
    trace->frames[count].method_id = method->find_jmethod_id_or_null();

    // 如果方法是不是 native 方法,则把 lineno 设置为 bci 的值,否则置 -3
    if (!method->is_native()) {
      trace->frames[count].lineno = bci;
    } else {
      trace->frames[count].lineno = -3;
    }
  }
  trace->num_frames = count;
  return;
}

总结

源码贴的有点多,这里稍微做一个小的总结,同时也说明一下使用时的一些注意事项

  • AsyncGetCallTrace 是 OpenJDK/OracleJDK 提供的可以在不暂停虚拟机的情况下可以获取线程栈的函数,开发人员的主要触发点是通过 UNIX 的 SIGPROF 信号来触发信号的 handler 来调用此函数,来随机获取某一个线程的栈,为高性能的热点方法监控提供了可行的技术支持;
  • AsyncGetCallTrace 的使用必须在 Agent onload 的时候 Enable JVMTI_EVENT_CLASS_LOAD 和 JVMTI_EVENT_CLASS_PREPARE 事件,不然无法获取相关的方法,同时还需要注册 callbacks.ClassPrepare 事件,在 class 加载准备阶段预先生成好 jMethodId,不然可能出现 jMethodId 为空的情况;
  • 实际上,AsyncGetCallTrace 还可以认为是标准 JVM-TI 中的 GetCallTrace 接口的线程安全版本。但是我们看到实际上,这个方法中所有代码都是未加锁的,为啥?细心的同学可能已经发现,因为该函数的调用是用信号处理函数调用,且只有某一个单独线程同时运行它,所以它的使用场景就天然决定了它是线程安全的。
  • 还有最后一点要注意,由于该方法的调用是在 java 线程中调用的,所以当使用者发送 SIGPROF 信号时,恰好由于进程处于 GC 阶段,而导致 java 线程处于安全点而被阻塞,从而导致此时无法执行该方法而获取线程栈的场景,或者在执行过程中线程被有关安全点挂起而导致获取线程栈失败这两个场景发生。

本文中难免存在一些错误和不足,还望大家不吝指出,同时如果对文中描述存在任何疑问,也欢迎大家提出来讨论。

以上就是深入剖析理解AsyncGetCallTrace源码的详细内容,更多关于AsyncGetCallTrace源码剖析的资料请关注我们其它相关文章!

(0)

相关推荐

  • Spring Boot异步调用@Async过程详解

    在实际开发中,有时候为了及时处理请求和进行响应,我们可能会多任务同时执行,或者先处理主任务,也就是异步调用,异步调用的实现有很多,例如多线程.定时任务.消息队列等, 我们来讲讲@Async异步方法调用. 一.@Async使用演示 @Async是Spring内置注解,用来处理异步任务,在SpringBoot中同样适用,且在SpringBoot项目中,除了boot本身的starter外,不需要额外引入依赖. 而要使用@Async,需要在启动类上加上@EnableAsync主动声明来开启异步方法. @

  • 深入理解spring boot异步调用方式@Async

    本文主要给大家介绍了关于spring boot异步调用方式@Async的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 1.使用背景 在日常开发的项目中,当访问其他人的接口较慢或者做耗时任务时,不想程序一直卡在耗时任务上,想程序能够并行执行,我们可以使用多线程来并行的处理任务,也可以使用spring提供的异步处理方式@Async. 2.异步处理方式 调用之后,不返回任何数据. 调用之后,返回数据,通过Future来获取返回数据 3.@Async不返回数据 使用@EnableAsyn

  • spring boot使用@Async异步注解的实现原理+源码

    1.java的大部分接口的方法都是串行执行的,但是有些业务场景是不需要同步返回结果的,可以把结果直接返回,具体业务异步执行,也有些业务接口是需要并行获取数据,最后把数据聚合在统一返回给前端. 通常我们都是采用多线程的方式来实现上述业务功能,但spring 提供更优雅的方式来实现上述功能,就是@Async 异步注解,在方法上添加@Async,spring就会借助AOP,异步执行方法. 1.如何启用@Async spring boot通过@EnableAsync 注解启用@Async异步注解 实现A

  • Spring中异步注解@Async的使用、原理及使用时可能导致的问题及解决方法

    前言 其实最近都在研究事务相关的内容,之所以写这么一篇文章是因为前面写了一篇关于循环依赖的文章: <Spring循环依赖的解决办法,你真的懂了吗> 然后,很多同学碰到了下面这个问题,添加了Spring提供的一个异步注解@Async循环依赖无法被解决了,下面是一些读者的留言跟群里同学碰到的问题: 本着讲一个知识点就要讲明白.讲透彻的原则,我决定单独写一篇这样的文章对@Async这个注解做一下详细的介绍,这个注解带来的问题远远不止循环依赖这么简单,如果对它不够熟悉的话建议慎用. 文章要点 @Asy

  • 深入剖析理解AsyncGetCallTrace源码底层原理

    目录 前言 源码实现 核心数据结构 函数申明 AsyncGetCallTrace 实现 pd_get_top_frame_for_signal_handler 实现 pd_get_top_frame 实现 forte_fill_call_trace_given_top 实现 总结 前言 AsyncGetCallTrace 是由 OracleJDK/OpenJDK 内部提供的一个函数,该函数可以在 JVM 未进入 safepoint 时正常获取到当前线程的调用栈(换句话说,使用该函数获取线程栈时,

  • Java同步锁Synchronized底层源码和原理剖析(推荐)

    目录 1 synchronized场景回顾 2 反汇编寻找锁实现原理 3 synchronized虚拟机源码 3.1 HotSpot源码Monitor生成 3.2 HotSpot源码之Monitor竞争 3.3 HotSpot源码之Monitor等待 3.4 HotSpot源码之Monitor释放 1 synchronized场景回顾 目标:synchronized回顾(锁分类–>多线程)概念synchronized:是Java中的关键字,是一种同步锁.Java中锁分为以下几种:乐观锁.悲观锁(

  • 解析从小程序开发者工具源码看原理实现

    如何查看小程序开发者工具源码 下面我们通过微信小程序开发者工具的源码来说说小程序的底层实现原理.以开发者工具版本号State v1.02.1904090的源码来窥探小程序的实现思路.如何查看微信源码,对于mac用户而言,查看微信小程序开发者工具的包内容,然后进入Contents/Resources/app.nw/js/core/index.js,注释掉如下代码就可以查看开发者工具渲染后的代码. // 打开 inspect 窗口 if (nw.App.argv.indexOf('inspect')

  • Python万物皆对象理解及源码学习

    目录 万物皆对象 1 类型对象和实例对象 2 类型.对象体系 2.1 元类型type 2.2 自定义类型 2.3 自定义类型子类 2.4 type和object的关系 3 可变对象与不可变对象 4 变长对象和定长对象 5 补充 万物皆对象 这篇博客的内容主要是针对Python中万物皆对象的理解,对Python的类型.对象体系做一个整体的梳理. 在Python中,一切皆为对象,一个整数是一个对象,一个字符串也是一个对象,基本类型(如int)也是对象.Python不再区别对待基本类型和对象,所有的基

  • React Context源码实现原理详解

    目录 什么是 Context Context 使用示例 createContext Context 的设计非常特别 useContext useContext 相关源码 debugger 查看调用栈 什么是 Context 目前来看 Context 是一个非常强大但是很多时候不会直接使用的 api.大多数项目不会直接使用 createContext 然后向下面传递数据,而是采用第三方库(react-redux). 想想项目中是不是经常会用到 @connect(...)(Comp) 以及 <Pro

  • 深入理解vue-class-component源码阅读

    vue-class-component是vue作者尤大推出的一个支持使用class方式来开发vue单文件组件的库.但是,在使用过程中我却发现了几个奇怪的地方. 首先,我们看一个简单的使用例子: // App.vue <script> import Vue from 'vue' import Component from 'vue-class-component' @Component({ props: { propMessage: String } }) export default clas

  • 非常实用的js验证框架实现源码 附原理方法

    本文为大家分享一个很实用的js验证框架实现源码,供大家参考,具体内容如下 关键方法和原理: function check(thisInput) 方法中的 if (!eval(scriptCode)) { return false; } 调用示例: 复制代码 代码如下: <input type="text" class="text_field percentCheck" name="progress_payment_two" id="

  • Andorid jar库源码Bolts原理解析

    Bolts: 作用: 用于链式执行跨线程代码,且传递数据 栗子: 复制代码 Task.call(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return true; } }, Task.UI_THREAD_EXECUTOR); Task.callInBackground(new Callable<Boolean>() { @Override public Boolean c

  • Java容器源码LinkedList原理解析

    LinkedList简介 LinkedList是一个使用双向链表结构实现的容器,与ArrayList一样,它能动态扩充其长度,LinkedList相较于ArrayList,其任意位置插入速度比ArrayList要快,但是其查询速度要比ArrayList要慢:LinkedList继承自AbstractSequentialList,实现了List.Deque.Cloneable.Serializable接口. LinkedList UML图如下: 和ArrayList一样,LinkedList也不是

  • 谈谈jQuery之Deferred源码剖析

    一.前言 大约在夏季,我们谈过ES6的Promise,其实在ES6前jQuery早就有了Promise,也就是我们所知道的Deferred对象,宗旨当然也和ES6的Promise一样,通过链式调用,避免层层嵌套,如下: //jquery版本大于1.8 function runAsync(){ var def = $.Deferred(); setTimeout(function(){ console.log('I am done'); def.resolve('whatever'); }, 10

随机推荐