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

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

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

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


TBProfiler


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


 运行监控



为了支持 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回调,可以获取到被捕获的异常信息。



这里有几点注意:

  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,并启动一个线程设置超时时间。如果达到阈值时主线程还没有获取到锁,我们就会去获取持有这个锁的线程的堆栈信息。当主线程获取到这个锁时,我们把收集到的信息进行上报。我们在达到阈值而不是主线程获取到锁时去抓堆栈,主要是因为主线程一旦获取到锁,我们就拿不到之前持有锁的线程的堆栈信息了。



这里我们看到主线程在加载动画时,因为触发调用了系统的getResourceValue方法,等待了Synchronized锁 917ms。而另一个线程也在操作AssetManager并持有了这个锁。



查看系统源码,他们都把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这个类实现线程的挂起和唤醒。


因为 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,通过异常信息进行上报聚合。



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

  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 具有很多个实例时,我们可以判断他存在泄露。



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



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


  • JVMTI 生成 Hprof


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



上图就是我们利用 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 的格式去获取和存储数据,可以自定义数据。



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



这里的数据可以包含对象的 ID、所属 Class、对象大小、引用和被引用对象的 ID 以及类型。加上所有 Class 的信息,我们可以生成一份自定义的 Mini Hprof 文件。虽然在生成时间上要比系统花费的更久,但是我们可以得到更小的数据。
针对ActivityFragment的泄漏, 我们可以在 Java 层通过 WeakReference 获取到关闭后仍然存活的对象类名,利用这个引用关系图,可以在单独的进程中计算出泄露对象的引用链并进行上报。如果不需要上报 RetainedSize,就不需要构建支配树,这样可以大大减轻端侧内存压力和处理速度。

总结和展望


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


团队介绍


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


相关文章
|
5月前
|
监控 Java Shell
JVMTI 在淘宝 Profiler 中的应用(上)
JVMTI 在淘宝 Profiler 中的应用()
|
存储 监控 Java
JVMTI 在手淘 Profiler 中的应用
JVMTI 在手淘 Profiler 中的应用
647 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
|
Java
JVM Profiler StacktraceCollectorProfiler
开篇  StacktraceCollectorProfiler主要用来采集线程的调用栈,原理是通过ManagementFactory.getThreadMXBean()返回的ThreadMXBean对象来实现。
794 0