JVMTI 在淘宝 Profiler 中的应用

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

JVMTI 在淘宝 Profiler 中的应用_第1张图片

Android JVMTI

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

  1. 运行状态监控

  2. 重定义 Class

  3. 跟踪对象分配和垃圾回收过程

  4. 遵循对象的引用树,遍历堆中的所有对象

  5. 检查 Java 调用堆栈

  6. 暂停和恢复所有线程

JVMTI 在淘宝 Profiler 中的应用_第2张图片

要使用 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 在淘宝 Profiler 中的应用_第3张图片

突破限制

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

  限制原理
JVMTI 在淘宝 Profiler 中的应用_第4张图片

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 中的应用_第5张图片

TBProfiler

JVMTI 的功能比较强大,但是使用不当可能对应用性能产生严重的影响。为了合理方便的使用 JVMTI 的能力,我们对 JVMTI 进行了封装成了 Android 的 Profiler 工具--“TBProfiler”,下面就介绍一下淘宝中主要使用的一些能力。

  运行监控

JVMTI 在淘宝 Profiler 中的应用_第6张图片

为了支持 JVMTI 功能,Android Runtime 从 8.0 开始引入了RuntimeCallbacks,用于统一管理运行时信息的回调通知。对于Heap和其他一些功能模块则是提供了对应的Listerner。JVMTI 则通过EventHandler统一进行管理,把相关接口暴露给 Agnet 使用。

  方法调用监控

JVMTI 提供了MethodEntryMethodExit回调来监听方法的执行和退出。包括应用和系统方法。所以打开之后对性能的影响还是很大的。线下可以用来监控方法调用,生成类似 SystemTrace 的信息。但更合适的场景是获取特定的一个或一类方法的调用频率、调用时长、调用堆栈等信息,帮助问题排查。这样可以大幅减少对性能的影响,在高端设备上的性能影响并不明显。
  线程和类监控

当线程创建或销毁、Class 加载时可以通过回调通知我们。这 2 个监控能力目前只在 C 层进行处理,并没有抛给 Java 层。因为在 Java 层处理可能存在嵌套的线程调用以及嵌套的 Class 加载。对于类加载我们目前主要是在线下使用,比如用来检查应用启动过程中加载的类和加载的顺序,配合方法调用的监控,辅助进行 Dex 重排、PGO 文件生成,来提升启动阶段的性能。

  异常捕获

JVMTI 提供了EventExceptionEventExceptionCatch两个回调来监听应用中发生的被捕获和未捕获的异常。很多时候我们写代码都是使用try-catch代替了条件检查,这样虽然代码不会崩溃,但是可能会导致一部分功能异常,却又难以发现。通过EventExceptionCatch回调,可以获取到被捕获的异常信息。
JVMTI 在淘宝 Profiler 中的应用_第7张图片

这里有几点注意:

  1. 系统自身有大量的捕获异常,可以根据异常方法所属的类进行过滤,过滤掉系统的异常

  2. 因为系统异常量很大,需要做采样提高性能

  3. 需要处理异常回调中递归的问题

  4. 开启后性能有严重影响,仅线下使用

  主线程锁监控

目前淘宝线上的一部分 ANR、卡顿问题可能都是由于死锁、主线程长时间等锁导致的。如果能在这些情况拿到持有锁线程的堆栈信息,可以帮助我们解决一部分问题。JVMTI 中提供了相应的回调事件以及获取运行信息的接口,利用这些能力我们在不需要 Hook 系统方法的情况下,实现了线上的主线程长等锁的监控。在平台上通过堆栈聚合,可以清楚的显示出那些操作导致了主线程的卡顿。

目前 Android 中锁的实现主要有两种:

  1. ART 实现:Monitor

  2. JDK 实现:AQS+CAS


  • Monitor

在 Java 层使用Synchronized,或者是Objectwaitnotify方法进行线程间的同步。在 ART 内是通过Monitor实现的,最终系统层面是通过自旋锁 + mutex 的方式来实现。

所有的Synchronized锁都有一个等待的Object对象,我们在主线程开始等锁的时候,获取到这个jobject,并启动一个线程设置超时时间。如果达到阈值时主线程还没有获取到锁,我们就会去获取持有这个锁的线程的堆栈信息。当主线程获取到这个锁时,我们把收集到的信息进行上报。我们在达到阈值而不是主线程获取到锁时去抓堆栈,主要是因为主线程一旦获取到锁,我们就拿不到之前持有锁的线程的堆栈信息了。

JVMTI 在淘宝 Profiler 中的应用_第8张图片

这里我们看到主线程在加载动画时,因为触发调用了系统的getResourceValue方法,等待了Synchronized锁 917ms。而另一个线程也在操作AssetManager并持有了这个锁。
JVMTI 在淘宝 Profiler 中的应用_第9张图片

查看系统源码,他们都把AssetManager对象作为锁。

public final class AssetManager implements AutoCloseable {
    @UnsupportedAppUsage
    boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
            boolean resolveRefs) {
        Objects.requireNonNull(outValue, "outValue");
        synchronized (this) {
            ensureValidLocked();
            final int cookie = nativeGetResourceValue(
                    mObject, resId, (short) densityDpi, outValue, resolveRefs);
            if (cookie <= 0) {
                return false;
            }

            // Convert the changing configurations flags populated by native code.
            outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
                    outValue.changingConfigurations);

            if (outValue.type == TypedValue.TYPE_STRING) {
                if ((outValue.string = getPooledStringForCookie(cookie, outValue.data)) == null) {
                    return false;
                }
            }
            return true;
        }
    }


    public @Nullable String[] list(@NonNull String path) throws IOException {
        Objects.requireNonNull(path, "path");
        synchronized (this) {
            ensureValidLocked();
            return nativeList(mObject, path);
        }
    }
}

目前线上大部分都是Synchroized锁,有了明确的堆栈信息,我们就可以结合实际场景来分析和优化,减少主线程的等待时间。当然可能并不是所有的 case 都可以直接进行解决和优化,但是有了这些信息可以帮助我们查找可能存在问题和可以优化的点。

  • AQS

Monitor不同,这个是 Java 实现,内部使用CLH队列 + CAS来实现线程同步。简单说就有一个state标识资源使用状态。可以独占也可以共享。线程请求资源时会定义为一个Node,获取到资源的通过CAS修改state,没有获取到锁的Node被添加到CLH队列中,然后挂起。当资源被释放时会唤起等待的Node,AQS 中通过LockSupport这个类实现线程的挂起和唤醒。
JVMTI 在淘宝 Profiler 中的应用_第10张图片

因为 AQS 锁的实现机制,所以无法像Synchroized锁那样直接确定谁持有了锁。但是可以通过抓取到的信息来分析这样的等待是否合理,是否有优化的空间。

  内存监控

JVMTI 中另一个比较重要的功能是内存监控。JVMTI 中提供了 4 个内存相关的回调,包括内存分配和回收、GC 开始和结束。

callback_->gcStart = MemoryUtils::HandleGCStart;
callback_->gcFinish = MemoryUtils::HandleGCFinish;
callback_->objectAlloc = MemoryUtils::HandleObjectAlloc;
callback_->objectFree = MemoryUtils::HandleObjectFree;

利用这些能力我们可以监控线上大内存的分配、总的内存分配和回收量、以及准确知道 GC 发生时间和次数。

void MemoryUtils::HandleObjectAlloc(jvmtiEnv *jvmti_env,
                                    JNIEnv *jni_env,
                                    jthread thread,
                                    jobject object,
                                    jclass object_klass,
                                    jlong size) {}

通过回调函数我们可以拿到分配对象的类型、大小以及线程。通过线程我们又可以获取到当前的堆栈信息。比如监控大内存的分配,当分配的大小超过阈值时,收集到的信息抛到 Java 层封装为一个BigMemoryAllocException,通过异常信息进行上报聚合。

JVMTI 在淘宝 Profiler 中的应用_第11张图片

这样就很容看到是那个线程在做什么操作分配了大内存。回到大内存监控实现本身,有一些问题是需要注意的:

  1. 性能问题:我们主要通过设定内存大小阈值和采样的方式减少对性能的影响。并且线上主要是为了收集数据,可以只针对高端机型采样开启;

  2. 数据收集问题:收集信息时我们主要会用到jclass和jthread两个对象来获取类型和堆栈。但是因为他们都是 local reference 对象,所以不能传递到其他线程,否则需要NewGlobalReference。所以在 C 层回调中只做必要信息收集,抛到 Java 层线程进行数据加工处理,减少性能影响;

  3. 内存分配递归:我们回调过程中处理数据时可能会再次触发内存分配,所以必须要做防递归的处理,我们使用thread_local 来记录每个线程的递归情况,发生递归则退出处理。


  内存 Dump

目前处理 Java 内存问题时,hprof 文件是最有效的方式。TBProfiler 中也有线上 Dump Hprof 的能力。虽然我们对 hprof 文件做了裁剪,但是体积还是比较大。从线上数据来看,在内存即将触顶(GC 回收后 HeapSize >= 95%)时,Dump 的 hprof 文件大小基本都在 700M 左右。通过裁剪(ImageSpaceZygoteSpace下的对象、原始数组的值、hprof 协议中使用不到字段等)和 zstd 压缩,最终得到的 hprof 文件在 90M 左右。整个过程对于用户磁盘空间(>1G)、网络条件(WIFI)都有一定要求。主要原因在于 hprof 文件内容大而全,而利用 JVMTI 能力,我们可以定制化的生成内存信息。


  • 对象实例信息


分析 hprof 时,我们可以通过 MAT 工具查看每个类的实例数量、Shallow Size 和 Retained Size 信息。我们可以通过分析对象实例数的方式判断那些组件存在泄露。比如某一个 Activity 具有很多个实例时,我们可以判断他存在泄露。

JVMTI 在淘宝 Profiler 中的应用_第12张图片

Hprof 虽然很好用,但是他很大。通过 JVMTI 遍历 Heap 堆上的对象(不包含不可达对象),我们可以实现类似的功能,区别在于无法提供 Retained Size,因为这个需要引用关系。

JVMTI 在淘宝 Profiler 中的应用_第13张图片

但是这个的目的是发现泄露,而不是定位泄露 。并且我们可以统计内存触顶时大致分布情况,经过聚合,可以对 Shallow Size 过大的类进行关注和进一步分析。并且生成的文件非常小,手淘内存触顶时,全量 Dump 所有对象实例信息,生成的文件经过压缩后只有不到 150K,可以方便快速的上报。快速的了解 OOM 时内存的分布状况。


  • JVMTI 生成 Hprof


实例信息只能帮我们发现一小部分问题,要定位和解决内存泄露,最重要的还是要能够拿到对象的之间的引用关系,找到异常实例无法释放的原因,我们还是需要依赖 Hprof 文件。既然 JVMTI 可以遍历 Heap 对象,我们是否可以利用 JVMTI 能力来生成 hprof 文件呢?

JVMTI 在淘宝 Profiler 中的应用_第14张图片

上图就是我们利用 JVMTI 能力生成符合 Android 标准的 Hprof 文件,可以成功了在 Android Studio 中打开。JDK 中就是利用 JVMTI 遍历 Heap 生成的,而 Android 中则是直接读取 Runtime 数据生成的。Android Hprof 文件主要包括:

  1. VERSION:JAVA PROFILE 1.0.3。这个不同于 JDK 的 JAVA PROFILE 1.0.2。

  2. LOAD CLASS:已加载类的列表,通过GetLoadedClasses 接口可以获取全部加载的类的信息;

  3. ROOT: GC Root 对象信息, 通过FollowReferences遍历对象间的引用关系来确定 GC ROOT 对象;

  4. CLASS DUMP: 这是类的详细信息,包含了 Super Class、ClassLoader、类的字段名、字段类型、静态字段值等信息。构建 CLASS DUMP 信息是整个过程中最复杂的。这里涉及到GetImplementedInterfacesGetClassSignatureGetClassFieldsGetFieldModifiersGetFieldName 等接口的调用;

  5. INSTANCE DUMP:这是实例对象的信息,主要是实例对象的 ID 和字段值。如果是类对象,那么他的值就是引用对象的 ID。这些值通过jvmtiPrimitiveFieldCallbackjvmtiStringPrimitiveValueCallbackjvmtiHeapReferenceCallback 获取。

  6. ARRAR DUMP:这个是数组,数组的值通过jvmtiArrayPrimitiveValueCallbackjvmtiHeapReferenceCallback 获取。

具体生成 hprof 过程还有很多需要注意的地方,因为 Android 本身 Dump 会有一些额外的操作,便于在 Android Studio 中显示更多的信息,这里就不介绍具体的生成过程了, 可以参考 Android 和 JDK 中生成 Hprof 的过程。


  • Mini Hprof


因为 Android 生成 Hprof 是直接在 Rumtime 中读取数据,所以相对于 JVMTI 的生成速度要快很多的,并且能获取更多的信息。通过 JVMTI 来生成 Hprof 在生成速度上有明显劣势,经过剪裁和压缩在文件大小上也没有明显优势。而且还有比较高的实现复杂度,生成的数据也不如系统生成的完备。看起来并没有很大的实用性。但是从中我们知道,通过 JVMTI 我们有能力收集到完整的 Heap 信息。这样我们就不必按照 Hprof 的格式去获取和存储数据,可以自定义数据。

JVMTI 在淘宝 Profiler 中的应用_第15张图片

对于排查内存泄漏问题,最重要的是获取到泄露对象到 GC Root 的引用信息。利用 JVMTI 遍历 Heap 的能力,我们可以方便的获取到所有对象之间的引用关系。并且可以区分出引用的类型,一般有 GCRoot 引用、实例字段引用、类静态字段引用、对象数组引用。

JVMTI 在淘宝 Profiler 中的应用_第16张图片

这里的数据可以包含对象的 ID、所属 Class、对象大小、引用和被引用对象的 ID 以及类型。加上所有 Class 的信息,我们可以生成一份自定义的 Mini Hprof 文件。虽然在生成时间上要比系统花费的更久,但是我们可以得到更小的数据。

针对ActivityFragment的泄漏, 我们可以在 Java 层通过 WeakReference 获取到关闭后仍然存活的对象类名,利用这个引用关系图,可以在单独的进程中计算出泄露对象的引用链并进行上报。如果不需要上报 RetainedSize,就不需要构建支配树,这样可以大大减轻端侧内存压力和处理速度。

75345e8da2d802882f19939540e2a42f.jpeg

总结和展望

目前在排查线上问题时,主要还是以捞取日志分析为主,但对于内存、卡顿、ANR 等性能相关的问题,日志并不能提供足够的信息进行分析。而 JVMTI 使得我们离 Runtime 更近一步,通过 JVMTI 的能力,我们能方便的获取到更丰富的运行时信息,辅助我们进行问题排查。但线上复杂的情况也不是一个 JVMTI 能完全覆盖的。我们需要类似 Android Studio 中的 Profiler 工具,从各个域收集相关的运行信息。从工具层面,完善工具链,提供高效稳定的线上 Profiler 工具;数据层面,整合工具的数据,生成自定义的 Profile 文件,以统一的方式对异常数据进行上报和分析,提高整体问题的定位排查效率。

737c3e20c09ba2c1e8fef5e92b37c7a5.jpeg

团队介绍

我们是淘天集团终端平台基础工程团队,立足于客户端技术, 主要负责手机淘宝的性能、稳定性、 编译构建、研发提效等。 团队致力于底层技术研究与新技术新趋势探索, 为上层业务提供技术支撑,为淘宝更小更快的运行保驾护航。

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

你可能感兴趣的:(JVMTI 在淘宝 Profiler 中的应用)