JVMTI 在淘宝 Profiler 中的应用(上)

简介: JVMTI 在淘宝 Profiler 中的应用()



JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 Native 编程接口。通过这些接口,开发人员不仅可以调试在虚拟机上运行的 Java 程序,还能查看它们运行的状态、控制环境变量,甚至修改代码逻辑,从而帮助开发人员监控和优化程序性能。



Android JVMTI


Android 的 JVMTI 功能是从 Android 8.0(API 26)开始支持的,官方的叫法是 ART Tooling Interface (ART TI) 。提供的重要功能主要有:

  1. 运行状态监控
  2. 重定义 Class
  3. 跟踪对象分配和垃圾回收过程
  4. 遵循对象的引用树,遍历堆中的所有对象
  5. 检查 Java 调用堆栈
  6. 暂停和恢复所有线程


要使用 JVMTI 的能力,需要提供一个 Agent,利用 ART TI 和 Runtime 进行通信。JVMTI 支持在 JVM 启动时和运行时加载这个 Agent。


dalvikvm -Xplugin:libopenjdkjvmti.so -agentpath:/path/to/agent/libagent.so …


VM 启动时加载不适用于 Android 应用,因为 Android 应用的进程都是从已运行的 zygote 进程 fork 出来的。所以只能在运行时加载。系统提供了 am 命令和 Debug 接口两种加载方式。

adb shell 'am attach-agent com.example.android.displayingbitmaps
\'
/data/data/com.example.android.displayingbitmaps/code_cache/libfieldnulls.so=Ljava/lang/Class;.name:Ljava/lang/String;\''


Debug 接口是通过attachJvmtiAgent加载,这个是 Android9.0 才开始提供。

/**

* Attach a library as a jvmti agent to the current runtime, with the given classloader

* determining the library search path.

* Note: agents may only be attached to debuggable apps. Otherwise, this function will

* throw a SecurityException.

*

* @param library the library containing the agent.

* @param options the options passed to the agent.

* @param classLoader the classloader determining the library search path.

*

* @throws IOException if the agent could not be attached.

* @throws a SecurityException if the app is not debuggable.

*/

public static void attachJvmtiAgent(@NonNull String library, @Nullable String options,

       @Nullable ClassLoader classLoader) throws IOException


Android8 上需要通过反射调用,但是反射的接口没有classLoader参数,这样会因为 namespace 问题导致 agent.so 无法调用第三方 so。一个简单的办法就是用一个空的 agent.so,加载它的作用只是用来初始化应用中的 JVMTI 环境。然后在通过System.load加载一个使用了 JVMTI 接口的 so 就可以了。


因为 JVMTI 提供了代码重定义的能力,所以 Android 上对 JVMTI 的功能进行了限制:

  1. 只能在可调式的 APP 包中使用 (android:debuggable = true)
  2. 未提供代码重定义相关的 Java 接口
  3. 对于 APP 进程来说,无法在进程启动时加载 Agent
  4. 尚未实现全部的 JVMTI 规范中的能力


突破限制


JVMTI 是一个强有力的监控工具,如果只能在 Debug 包上运行,那么实用性将大大降低。而且 Debug 包得到的数据本身就和实际线上环境相差很大,所以要想利用好 JVMTI,第一步就是要突破 Debug 包的限制。


 限制原理



Agent 的加载主要包括两步:

  1. EnsureJvmtiPlugin: 加载 libopenjdkjvmti.so, 初始化进程的 JVMTI 运行环境
  2. AgentSpec.Attach: 加载 agent.so, 使用 JVMTI 的能力


static bool EnsureJvmtiPlugin(Runtime* runtime,
                              std::string* error_msg) {
  // TODO Rename Dbg::IsJdwpAllowed is IsDebuggingAllowed.
  DCHECK(Dbg::IsJdwpAllowed() || !runtime->IsJavaDebuggable())
      << "Being debuggable requires that jdwp (i.e. debugging) is allowed.";
  // Is the process debuggable? Otherwise, do not attempt to load the plugin unless we are
  // specifically allowed.
  if (!Dbg::IsJdwpAllowed()) {
    *error_msg = "Process is not allowed to load openjdkjvmti plugin. Process must be debuggable";
    return false;
  }
  constexpr const char* plugin_name = kIsDebugBuild ? "libopenjdkjvmtid.so" : "libopenjdkjvmti.so";
  return runtime->EnsurePluginLoaded(plugin_name, error_msg);
}


从源码中我们发现了限制 Debug 包的地方主要有两个接口:

  1. Dbg::IsJdwpAllowed()
  2. Runtime::IsJavaDebuggable()


这两个接口中返回的变量是在进程从ZygoteHooks_nativePostForkChild中设置的

static uint32_t EnableDebugFeatures(uint32_t runtime_flags) {
  Runtime* const runtime = Runtime::Current();
  // 设置JDWP
  Dbg::SetJdwpAllowed((runtime_flags & DEBUG_ENABLE_JDWP) != 0);
  if ((runtime_flags & DEBUG_ENABLE_JDWP) != 0) {
    EnableDebugger();
  }
  runtime_flags &= ~DEBUG_ENABLE_JDWP;
  // 设置Runtime的DEBUG_JAVA_DEBUGGABLE
  bool needs_non_debuggable_classes = false;
  if ((runtime_flags & DEBUG_JAVA_DEBUGGABLE) != 0) {
    runtime->AddCompilerOption("--debuggable");
    runtime_flags |= DEBUG_GENERATE_MINI_DEBUG_INFO;
    runtime->SetJavaDebuggable(true);
    // Deoptimize the boot image as it may be non-debuggable.
    runtime->DeoptimizeBootImage();
    runtime_flags &= ~DEBUG_JAVA_DEBUGGABLE;
    needs_non_debuggable_classes = true;
  }
  // 还有native debug相关,对JVMTI影响不大
  ...
  return runtime_flags;
}


这两个 flag 实际是由android:debuggable决定的,但是线上包设置为 true 开启 debug 是不可行的。


 修改 JDWP


//Source: platform/art/runtime/debugger.cc
// JDWP is allowed unless the Zygote forbids it.
static bool gJdwpAllowed = true;
bool Dbg::IsJdwpAllowed() {
  return gJdwpAllowed;
}
void Dbg::SetJdwpAllowed(bool allowed) {
  gJdwpAllowed = allowed;
}


从源码看到,gJdwpAllowed是一个静态变量。我们可以通过dlsym的方式获取到SetJdwpAllowed方法符号来修改他的状态。需要注意的是,这个符号在libart.so中,Android 7.0 对加载系统库进行了限制。不过目前绕过的方式都比较成熟,我们采用了读取/proc/self/maps查找 libart.so 和解析 ELF 的方式获取到了此方法的符号。

 修改 Runtime


//Source: platform/art/runtime/runtime.h
// Whether Java code needs to be debuggable.
bool is_java_debuggable_;
bool IsJavaDebuggable() const {
    return is_java_debuggable_;
}
void Runtime::SetJavaDebuggable(bool value) {
  is_java_debuggable_ = value;
  // Do not call DeoptimizeBootImage just yet, the runtime may still be starting up.
}


is_java_debuggable_Runtime中的一个成员变量,没办法采用和 JDWP 一样的方式进行修改。但是在 C 层我们通JavaVM对象可以拿到唯一Runtime的对象。

class JavaVMExt : public JavaVM {
private:
  Runtime* const runtime_;
}


然后通过内存布局找到is_java_debuggable_地址进行修改。虽然我们针对不同版本进行适配, 但是手机厂商可能会对 Runtime 结构进行修改,这就会导致一旦修改了错误的地址,可能会引发 Crash。

class Runtime {
  // Specifies target SDK version to allow workarounds for certain API levels.
  int32_t target_sdk_version_;
  CompatFramework compat_framework_;
  bool implicit_null_checks_;       // NullPointer checks are implicit.
  bool implicit_so_checks_;         // StackOverflow checks are implicit.
  bool implicit_suspend_checks_;    // Thread suspension checks are implicit.
  bool no_sig_chain_;
  bool force_native_bridge_;
  bool is_native_bridge_loaded_;
  bool is_native_debuggable_;
  bool async_exceptions_thrown_;
  bool non_standard_exits_enabled_;
  // Whether Java code needs to be debuggable.
  bool is_java_debuggable_;
}


从源码看到,在这个字段之前,有一些字段的值是相对固定的,我们可以通过这些字段作为参考点,来更准确的找到is_java_debuggable_。这里我们主要选取了target_sdk_version_作为结构起始的参考点,而后面几个 bool 值变量,则成为我们检验的字段,因为他们的值也是相对固定的。

Android12 之后多了一个compat_framework_字段,使用偏移量进行计算容易出错,所以我们自己定义了MockRuntime对象,以target_sdk_version_地址为起点进行对象转换。这里又涉及到CompatFramework 内部的结构以及内存对齐的问题,这里就不展开了。

对 JDWP 和 Rumtime 修改之后,Agent 就可以成功的在 Release 环境加载了,我们在加载完成后会恢复原有的值,保证不影响 APP 的正常运行。针对线上加载失败的情况,我们把 Runtime 的内存 Dump 后进行了线下分析,发现基本都是在一些双开模拟器中获取不到target_sdk_version_导致失败。所以我们也增加了多开设备的开关。

 使用受限版本
通过对 ART 中 JVMTI 源码的深入分析,我们发现源码中提供给了一个kArtTiVersion的版本号。


// A special version that we use to identify special tooling interface versions which mostly matches
// the jvmti spec but everything is best effort. This is used to implement the userdebug
// 'debug-anything' behavior.
//
// This is the value 0x70010200.
static constexpr jint kArtTiVersion = JVMTI_VERSION_1_2 | 0x40000000;


当 JVMTI 以这个版本号运行时,他只提供了部分功能,而使用JVMTI_VERSION_1_2版本时则提供了完整功能。从代码可以看到,如果运行在 Debug 环境或者完全解释执行的环境时,所有功能可用。也就是说这个受限版本可以在 Release 包使用。


// Returns whether we are able to use all jvmti features.
static bool IsFullJvmtiAvailable() {
  art::Runtime* runtime = art::Runtime::Current();
  return runtime->GetInstrumentation()->IsForcedInterpretOnly() || runtime->IsJavaDebuggable();
}


通过源码分析,我们发现使用受限版本只是无法使用调试和热修复的能力,但是监控和获取运行信息的能力基本不受影响。也从另一个方面说明 JVMTI 进行线上监控的可行性。


// These are capabilities that are disabled if we were loaded without being debuggable.
//
// This includes the following capabilities:
//   can_retransform_any_class:
//   can_retransform_classes:
//   can_redefine_any_class:
//   can_redefine_classes:
//   can_pop_frame:
//   can_force_early_return:
//     We need to ensure that inlined code is either not present or can always be deoptimized. This
//     is not guaranteed for non-debuggable processes since we might have inlined bootclasspath code
//     on a threads stack.


考虑到后续可能会使用到更多的能力,所以我们最终没有使用受限版本,还是通过 Mock 方式开启了全功能版本。但是要注意的是,即便使用了全功能版本,要使用全部的能力,还有许多事情需要解决。



更多精彩内容,欢迎观看:

JVMTI 在淘宝 Profiler 中的应用(下):https://developer.aliyun.com/article/1396407

相关文章
|
5月前
|
存储 监控 Java
JVMTI 在淘宝 Profiler 中的应用(下)
JVMTI 在淘宝 Profiler 中的应用(下)
148 6
|
存储 监控 Java
JVMTI 在手淘 Profiler 中的应用
JVMTI 在手淘 Profiler 中的应用
645 0
JVMTI 在手淘 Profiler 中的应用
|
Arthas Oracle 安全
cpu分析利器 — async-profiler
async-profiler是一款采集分析java性能的工具,翻译一下github上的项目介绍
507 0
cpu分析利器 — async-profiler
|
Web App开发 SQL JavaScript
【性能】7分钟带你了解【尤大】都在使用的 Chrome Runtime Performance Debug!
前段时间用Vue3开发了一个线上标准的项目,也都已经提测了,最近可以抽时间来整理一下项目中用到的技术栈,( 我用前端【最新】技术栈完成了一个生产标准的项目【Vue3 + TS + Vite + Pinia + Windicss + NavieUI】。 在开发之前特地重温了一下18年尤大的VUE CONF(杭州),过程中发现尤大使用到了这个Performance去分析JavaScript每一帧的计算,之前知道这个工具,但是平时基本不怎么用,借此机会学一下并做一些记录。
179 0
|
监控 安全 Android开发
借你一双慧眼, Frida Native Trace
借你一双慧眼, Frida Native Trace
借你一双慧眼, Frida Native Trace
|
分布式计算 监控 Java
Uber jvm profiler 使用
Uber jvm profiler 使用
243 0
Uber jvm profiler 使用
|
监控 Java
troubleshoot之:使用JFR分析性能问题
troubleshoot之:使用JFR分析性能问题
troubleshoot之:使用JFR分析性能问题
|
Java 程序员 C++
troubleshoot之:使用JFR解决内存泄露
troubleshoot之:使用JFR解决内存泄露
troubleshoot之:使用JFR解决内存泄露
|
NoSQL Java Redis
JVM Profiler Reporter介绍
开篇  JVM Profiler采集完数据后可以通过多种途径上报数据,对接Console,File,redis,kafka等,这篇文章会把源码罗列一下毕竟都很简单。
1143 0
|
数据采集 Linux 索引
JVM Profiler IOProfiler
开篇  IOProfiler因为采集方法的限制,目前支持linux系统指标采集,但是不支持mac,windows等操作系统。  IOProfiler通过读取linux系统的/proc/self/io的当前线程的IO指标数据,该文件的内容如下图所示,通过解析成kv键值对完成采集。
999 0