稀土掘金 稀土掘金

基于JVMTI 实现性能监控

什么是JVMTI

JVMTI 全程 JVM Tool Interface,它是Java虚拟机定义的一个开发和监控JVM使用的程序接口(programing interface),通过该接口可以探查JVM内部的一些运行状态,甚至控制JVM应用程序的执行。 需要注意的是,并非所有的JVM实现都支持JVMTI。

JVMTI是双通道接口(two-way interface)。JVMTI的客户端,或称为代理(agent),agent可以通过注册监听感兴趣的事件,另外,JVMTI提供了很多操作函数可以直接用来控制应用程序。

JVMTI代理与目标JVM运行在同一个进程中,通过JVMTI进行通信,最大化控制能力,最小化通信成本

JVMTI 接口提供的监控及控制能力

关于JVMTI规范提供的所有能力可以在 docs.oracle.com/javase/8/do… 文档中查看,这里简单罗列下一些功能,让我们大概对其能力有个了解。

Function (提供的控制函数)

内存 (VM Heap Memory)

  • 分配内存 (Allocate Memory)
  • 释放分配的内存 (DeAllocate Memory)

线程

  • 获取线程状态 (GetThreadState)
  • 获取当前线程 (GetCurrentThread)
  • 获取所有的线程 (GetAllThreads)
  • 暂停线程(SuspendThread)
  • ...

Stack Frame

  • 获取某个线程当前的调用栈 (GetStackTrace)
  • 获取所有线程的调用栈 (GetAllStackTraces)
  • 弹出栈帧 (PopFrame)

Force Early Return

  • 提前返回函数

Class、Object、Method、Field

  • 根据jclass、jobject 、jfieldID、 jmethodID 等获取类、对象、函数、字段的相关信息

事件监听

以下是完整的事件监听支持的类型

typedef struct {
                              /*   50 : VM Initialization Event */
    jvmtiEventVMInit VMInit;
                              /*   51 : VM Death Event */
    jvmtiEventVMDeath VMDeath;
                              /*   52 : Thread Start */
    jvmtiEventThreadStart ThreadStart;
                              /*   53 : Thread End */
    jvmtiEventThreadEnd ThreadEnd;
                              /*   54 : Class File Load Hook */
    jvmtiEventClassFileLoadHook ClassFileLoadHook;
                              /*   55 : Class Load */
    jvmtiEventClassLoad ClassLoad;
                              /*   56 : Class Prepare */
    jvmtiEventClassPrepare ClassPrepare;
                              /*   57 : VM Start Event */
    jvmtiEventVMStart VMStart;
                              /*   58 : Exception */
    jvmtiEventException Exception;
                              /*   59 : Exception Catch */
    jvmtiEventExceptionCatch ExceptionCatch;
                              /*   60 : Single Step */
    jvmtiEventSingleStep SingleStep;
                              /*   61 : Frame Pop */
    jvmtiEventFramePop FramePop;
                              /*   62 : Breakpoint */
    jvmtiEventBreakpoint Breakpoint;
                              /*   63 : Field Access */
    jvmtiEventFieldAccess FieldAccess;
                              /*   64 : Field Modification */
    jvmtiEventFieldModification FieldModification;
                              /*   65 : Method Entry */
    jvmtiEventMethodEntry MethodEntry;
                              /*   66 : Method Exit */
    jvmtiEventMethodExit MethodExit;
                              /*   67 : Native Method Bind */
    jvmtiEventNativeMethodBind NativeMethodBind;
                              /*   68 : Compiled Method Load */
    jvmtiEventCompiledMethodLoad CompiledMethodLoad;
                              /*   69 : Compiled Method Unload */
    jvmtiEventCompiledMethodUnload CompiledMethodUnload;
                              /*   70 : Dynamic Code Generated */
    jvmtiEventDynamicCodeGenerated DynamicCodeGenerated;
                              /*   71 : Data Dump Request */
    jvmtiEventDataDumpRequest DataDumpRequest;
                              /*   72 */
    jvmtiEventReserved reserved72;
                              /*   73 : Monitor Wait */
    jvmtiEventMonitorWait MonitorWait;
                              /*   74 : Monitor Waited */
    jvmtiEventMonitorWaited MonitorWaited;
                              /*   75 : Monitor Contended Enter */
    jvmtiEventMonitorContendedEnter MonitorContendedEnter;
                              /*   76 : Monitor Contended Entered */
    jvmtiEventMonitorContendedEntered MonitorContendedEntered;
                              /*   77 */
    jvmtiEventReserved reserved77;
                              /*   78 */
    jvmtiEventReserved reserved78;
                              /*   79 */
    jvmtiEventReserved reserved79;
                              /*   80 : Resource Exhausted */
    jvmtiEventResourceExhausted ResourceExhausted;
                              /*   81 : Garbage Collection Start */
    jvmtiEventGarbageCollectionStart GarbageCollectionStart;
                              /*   82 : Garbage Collection Finish */
    jvmtiEventGarbageCollectionFinish GarbageCollectionFinish;
                              /*   83 : Object Free */
    jvmtiEventObjectFree ObjectFree;
                              /*   84 : VM Object Allocation */
    jvmtiEventVMObjectAlloc VMObjectAlloc;
} jvmtiEventCallbacks;

这里列举一下一些常用的功能

  • 线程相关事件
    • 线程启动 (ThreadStart)
    • 线程结束 (ThreadEnd)
    • ...
  • 类加载事件
    • 类文件加载 (ClassFileLoadHok)
    • 类被加载到虚拟机 (ClassLoad)
    • 类准备阶段完成 (ClassPrepare)
  • 异常事件
  • 方法执行
    • 方法开始执行 (MethodEntry)
    • 方法执行结束 (MethodExit)
    • ...
  • GC事件
    • GC启动 (GarbageCollectionStart)
    • GC结束 (GarbageCollectionFinish)
  • 对象事件
    • 为一个对象分配内存 (VMObjectAlloc)
    • 释放一个对象所占的内存 (ObjectFree)

JVMTI Header

完整的 JVMTI API可以通过 jvmti.h 获取,在Android中可以通过 此链接 查看。

JvmTi 在 Android中的实现

Android虚拟机直到 8.0系统开始才实现了 JVMTI 1.2,从当前的系统版本发布来看,8.0可以覆盖65%以上的用户设备。在Android中 JvmTi 又被称为 ART Ti(毕竟是ART虚拟机),它增加了一些限制如(摘自官网说明):

  1. 首先,提供代理接口 JVMTI 的代码作为运行时插件(而不是运行时的核心组件)来实现。插件加载可能会受到限制,这样可阻止代理找到任何接口点
  • 其次,ActivityManager 类和运行时进程只允许代理连接到可调试的应用。可调试应用由其开发者签核,以供分析和插桩,而不会分发给最终用户。Google Play 商店不允许发布可调试应用。这可确保普通应用(包括核心组件)无法遭到检测或操纵。

在Android 中 JVMTI 和Agent的架构如图 image.png

在Android中 使用JVMTI有两种方式

  1. 虚拟机启动时连接代理
  2. 运行时。将代理加载到当前进程中

关于两种方式的具体使用流程亦可参照 官网介绍,这里不做详述。其jvmti接口的具体实现可以在 这里找到。其最终被编译成 libopenjdkjvmti.so。 以我的64位手机为例,在 目录 /lib64/system/libopenjdkjvmti.so 可以找到该动态库 image.png

JVMTI 的一些应用实例

这里简单介绍写 JVMTI 的一些应用。 以Android官方为例

  • 基于JVMTI (重定义类功能)实现 Apply Changes功能
    • 这部分 agent 实现可以在 这里找到,从这里你可以了解基于jvmti实现热加载的核心逻辑
  • Android Studio Profiler 的部分功能
    • 内存分配监控
    • GC监控
    • 通过替换dex 中的class 实现对系统类的各种监控 (ClassTransform)
  • JVMTI 是JDI 体系的后端实现,我们可以自己基于JVMTI实现程序调试能力,比如美团有一篇文章介绍了基于JDWP 实现了线上调试的功能,关于JDPA体系可以参考 oracle文档 ,以下是 JVMTI、JDWP、JDI的一个关系结构图

image.png 这里想到了另外一些思路,如果可以突破 art虚拟机对于debugger  =false无法使用jvmti的限制,是否可以在线上实现更强大的性能监控,或者是在线下环境 脱离Android Studio 实现性能采集工具? 首先确定基于JVMTI强大的能力,我们在线下至少可以实现以下功能

  • 基于获取所有线程堆栈的能力,使用采样的方式 生成函数调用火焰图
  • 基于 对象内存分配、释放函数实现 内存监控
  • 基于 MonitorContended 实现 锁等待监控
  • 基于 GarbageCollection 实现GC时长的监控
  • ...

在release 包(debugger =false)中使用 JVMTI

之前介绍了,由于Android 虚拟机的限制,默认情况下,JVMTI Agent只能在非debug包中被动态加载。因此我们首先来了解下Android 是如何进行限制的

JVMTI 使用环境限制

    public static void attachJvmtiAgent(@NonNull String library, @Nullable String options,
            @Nullable ClassLoader classLoader) throws IOException {
        Preconditions.checkNotNull(library);
        Preconditions.checkArgument(!library.contains("="));

        if (options == null) {
            VMDebug.attachAgent(library, classLoader);
        } else {
            VMDebug.attachAgent(library + "=" + options, classLoader);
        }
    }

Debug类的 attatchJvmtiAgent最终调用了 VMDebug.attatchAgent函数,该函数最终调用了 native层 art/runtime/native/dalvik_system_VMDebug.cc 的nativeAttatchAgent函数

static void VMDebug_nativeAttachAgent(JNIEnv* env, jclass, jstring agent, jobject classloader) {
  if (agent == nullptr) {
    ScopedObjectAccess soa(env);
    ThrowNullPointerException("agent is null");
    return;
  }
	
  // 判断是否允许Jdwp
  if (!Dbg::IsJdwpAllowed()) {
    ScopedObjectAccess soa(env);
    ThrowSecurityException("Can't attach agent, process is not debuggable.");
    return;
  }

  std::string filename;
  {
    ScopedUtfChars chars(env, agent);
    if (env->ExceptionCheck()) {
      return;
    }
    filename = chars.c_str();
  }

  Runtime::Current()->AttachAgent(env, filename, classloader);
}



//art/runtime/debugger.cc

// JDWP is allowed unless the Zygote forbids it.
static bool gJdwpAllowed = true;

// 设置 Jdwp可用
void Dbg::SetJdwpAllowed(bool allowed) {
  gJdwpAllowed = allowed;
}

//设置 Jdwp不可用
bool Dbg::IsJdwpAllowed() {
  return gJdwpAllowed;
}

VMDebug_nativeAttachAgent 函数是通过debugger.cc中国的 gJdwpAllowed 变量来判断 ,从定义上可以看出该变量默认 为true。我们继续跟踪下该变量是在何处被修改的, 最终跟踪到 该变量是在zygote进程进行fork时配置的

static void ZygoteHooks_nativePostForkChild(JNIEnv* env,
                                            jclass,
                                            jlong token,
                                            jint runtime_flags,
                                            jboolean is_system_server,
                                            jboolean is_zygote,
                                            jstring instruction_set) {
  DCHECK(!(is_system_server && is_zygote));
  // Set the runtime state as the first thing, in case JIT and other services
  // start querying it.
  Runtime::Current()->SetAsZygoteChild(is_system_server, is_zygote);

  Thread* thread = reinterpret_cast<Thread*>(token);
  // Our system thread ID, etc, has changed so reset Thread state.
  thread->InitAfterFork();
  // 配置 Debug程序 的一些特性
  runtime_flags = EnableDebugFeatures(runtime_flags);
  
}


static uint32_t EnableDebugFeatures(uint32_t runtime_flags) {
  Runtime* const runtime = Runtime::Current();
  if ((runtime_flags & DEBUG_ENABLE_CHECKJNI) != 0) {
    JavaVMExt* vm = runtime->GetJavaVM();
    if (!vm->IsCheckJniEnabled()) {
      LOG(INFO) << "Late-enabling -Xcheck:jni";
      vm->SetCheckJniEnabled(true);
      // There's only one thread running at this point, so only one JNIEnv to fix up.
      Thread::Current()->GetJniEnv()->SetCheckJniEnabled(true);
    } else {
      LOG(INFO) << "Not late-enabling -Xcheck:jni (already on)";
    }
    runtime_flags &= ~DEBUG_ENABLE_CHECKJNI;
  }

  if ((runtime_flags & DEBUG_ENABLE_JNI_LOGGING) != 0) {
    gLogVerbosity.third_party_jni = true;
    runtime_flags &= ~DEBUG_ENABLE_JNI_LOGGING;
  }
	
  
  //配置 debugger.cc 的  gJdwpAllowed变量
  Dbg::SetJdwpAllowed((runtime_flags & DEBUG_ENABLE_JDWP) != 0);
  runtime_flags &= ~DEBUG_ENABLE_JDWP;

  const bool safe_mode = (runtime_flags & DEBUG_ENABLE_SAFEMODE) != 0;
  if (safe_mode) {
    // Only quicken oat files.
    runtime->AddCompilerOption("--compiler-filter=quicken");
    runtime->SetSafeMode(true);
    runtime_flags &= ~DEBUG_ENABLE_SAFEMODE;
  }
}

总结一下: 当启动我们的应用程序时,zygote在进行forkProcess 时,根据 runtime_flgs中的标记位调用 Dbg::SetJdwpAllowed函数,当为非debug包时,gJdwpAllowed变量被置为false,因此在运行时执行Debug.attachJvmtiAgent 函数时 会抛出异常。

突破限制

上节,简单分析了JDWP在非debug包下的限制,我们可以很容易想到通过在运行时将 gJdwpAllowed的变量值修改为true,再进行 attachJvmtiAgent的函数调用。 这里我使用了 SandHook库来实现这个功能, Dbg::setJdwoAllowed对应的函数符号 为 _ZN3art3Dbg14SetJdwpAllowedEb (64位下) ,

SandHook函数主要是实现InlineHook的功能,本文的介绍其实并不需要InlineHook,只是我的项目中使用了InlineHook来实现其他的功能。 事实上这里你只需要使用能够 破解Android  dlopen限制的函数 即可,如 dlfunciotns

    if (sizeof(size_t) == 8) {
        if (fileExits("/apex/com.android.runtime/lib64/libart.so")) {
            ArtLibPath = "/apex/com.android.runtime/lib64/libart.so";
        } else {
            ArtLibPath = "/system/lib64/libart.so";
        }
        auto (*SetJdwpAllowed)(bool) = reinterpret_cast<void (*)(bool)>(SandGetSym(ArtLibPath,
                                                                                   "_ZN3art3Dbg14SetJdwpAllowedEb"));

        if (SetJdwpAllowed != nullptr) {
            SetJdwpAllowed(true);
        }
          
        auto
        (*setJavaDebuggable)(void *, bool) = reinterpret_cast<void (*)(void *, bool)>(SandGetSym(
                ArtLibPath,
                "_ZN3art7Runtime17SetJavaDebuggableEb"));
        if (setJavaDebuggable != nullptr) {
            setJavaDebuggable(ArtHelper::getRuntimeInstance(), true);
            ALOGE("zxw %s", "setJavaDebuggable true");
        }


    }

修改变量之后 再进行 attatchJvmtiAgent函数的调用,此时程序不会在崩溃了,然而 Jdwp Agent还是加载失败了,从日志上可以看到 控制台输出的日志

Openjdkjvmti plugin was loaded on a non-debuggable Runtime. Plugin was loaded too late to change runtime state to DEBUGGABLE. Only kArtTiVersion (0x70010200) environments are available. Some functionality might not work properly.

在源码中搜索上述日志,我们找到了相应代码 image.png 上述日志的输出执行时机点是在Agent被attach到进程时输出的,在 Agent_OnAttach回调函数中,获取 JvmTiEnv时失败了

jint SetupJvmtiEnv(JavaVM *vm,jvmtiEnv** jvmti) {
    jint res =  vm->GetEnv(reinterpret_cast<void **>(jvmti), JVMTI_VERSION_1_2);
    if (res != JNI_OK || jvmti == nullptr) {
        ALOGE("==========Agent vm->GetEnv VERSION_1_2 failed");
    }

    return res;
}

因此,我们继续深入代码,从Jvmti源码中查找 返回JvmTiEnv指针的部分代码, 这部分实现是在OpenjdkJvmTI.cc中image.png

继续跟进 IsFullJvmtiAvailable 的实现 image.png

image.png

原来在 源码中 获取Jvmti指针时,是依据 Runtime类的 IsJavaDebuggable()函数来判断的,并不直接依赖于Dbg类,虽然我们 已经修改了Dbg类的 gJdwpAllowed变量,然而 Runtime的 is_java_debuggable_变量在Process Fork阶段就已经被修改了,我们对Dbg::gJdwpAllowed的滞后修改并不能影响不直接依赖于Dbg::gJdwpAllowed变量的地方,因此我们还需要手动修改Runtime对象的该变量。 修改的方式和修改Dbg变量的方式一样,代码如下


auto
(*setJavaDebuggable)(void *, bool) = reinterpret_cast<void (*)(void *, bool)>(SandGetSym(
ArtLibPath,"_ZN3art7Runtime17SetJavaDebuggableEb"));

if (setJavaDebuggable != nullptr) {
  setJavaDebuggable(ArtHelper::getRuntimeInstance(), true);
}

在修改后,在非debug包下进行了测试,基本功能和debug包一致 image.png 这里我只监控了GC事件、线程创建销毁、对象分配这三类事件,其他事件是否能正常使用未经过充分测试,从原理上讲,JVMTI的实现和大部分系统、虚拟机提供监控实现机制差不多,都是通过在源码中事先写入相关监控代码实现的,只要监控点的代码是依赖于Dbg和Runtime的那两个变量那么都应该能正常工作。

线上 使用 JVMTI进行性能监控的评估点

上节中,我们实现了在 生产环境使用 JVMTI的能力,然而这离将JVMTI真正运用在线上生产环境还有很多需要考虑的点如:

  • 兼容性
    • 只能在8.0及以上系统使用
    • 依赖于 dlsym 调用系统函数,不排查Android官方或者第三方厂商对这些函数进行了修改 需要测试兼容性
  • 性能
    • 开启JVMTI后 需要评估对相关性能的影响点

最后对APM方向感兴趣的同学可以关注下我的性能监控专栏,会持续分享更多性能监控、优化相关的博文: juejin.cn/column/7107… ,专栏历史文章:

文章地址
监控Android Looper Message调度的另一种姿势 juejin.cn/post/713974…
Android 高版本采集系统CPU使用率的方式 juejin.cn/post/713503…
Android 平台下的 Method Trace 实现及应用 juejin.cn/post/710713…
Android 如何解决使用SharedPreferences 造成的卡顿、ANR问题 juejin.cn/post/705476…
基于JVMTI 实现性能监控 juejin.cn/post/694278…

参考文献

  • docs.oracle.com/javase/8/do…
  • docs.oracle.com/javase/7/do…

装修网北京合建装饰怎么样装修格栅20x20多少一个平方室内装修行业怎么样怎样装修装饰公司简装120平方需多少钱123平米欧式装修不如装修武汉锋利装饰装饰erp巴乐兔装修装饰开州结婚头车装饰天津公装装修公司120平方米简装公装的装修公司装修一般得多少钱[家庭装修]30万的装修宁波中煌装饰茂名装饰装修装修简装是什么意思恩林装饰怎么样郑州88平方米简装多少钱顶楼露台装修设计宁波公装公司简装一套90平米的房子要多少钱工程装修装饰合同崇阳装修买了精装房还要哪些装修合肥简装公司香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声汪小菲曝离婚始末卫健委通报少年有偿捐血浆16次猝死单亲妈妈陷入热恋 14岁儿子报警雅江山火三名扑火人员牺牲系谣言手机成瘾是影响睡眠质量重要因素男子被猫抓伤后确诊“猫抓病”中国拥有亿元资产的家庭达13.3万户高校汽车撞人致3死16伤 司机系学生315晚会后胖东来又人满为患了男孩8年未见母亲被告知被遗忘张家界的山上“长”满了韩国人?倪萍分享减重40斤方法许家印被限制高消费网友洛杉矶偶遇贾玲何赛飞追着代拍打小米汽车超级工厂正式揭幕男子被流浪猫绊倒 投喂者赔24万沉迷短剧的人就像掉进了杀猪盘特朗普无法缴纳4.54亿美元罚金周杰伦一审败诉网易杨倩无缘巴黎奥运专访95后高颜值猪保姆德国打算提及普京时仅用姓名西双版纳热带植物园回应蜉蝣大爆发七年后宇文玥被薅头发捞上岸房客欠租失踪 房东直发愁“重生之我在北大当嫡校长”校方回应护栏损坏小学生课间坠楼当地回应沈阳致3死车祸车主疑毒驾事业单位女子向同事水杯投不明物质路边卖淀粉肠阿姨主动出示声明书黑马情侣提车了奥巴马现身唐宁街 黑色着装引猜测老人退休金被冒领16年 金额超20万张立群任西安交通大学校长王树国卸任西安交大校长 师生送别西藏招商引资投资者子女可当地高考胖东来员工每周单休无小长假兔狲“狲大娘”因病死亡外国人感慨凌晨的中国很安全恒大被罚41.75亿到底怎么缴考生莫言也上北大硕士复试名单了专家建议不必谈骨泥色变“开封王婆”爆火:促成四五十对测试车高速逃费 小米:已补缴天水麻辣烫把捣辣椒大爷累坏了

装修网 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化