基于JVMTI 实现性能监控

本文详细介绍了如何在Android非debug环境下利用JVMTI进行性能监控,包括 JVMTI的原理、Android的限制、沙盒Hook技术的应用,以及线上使用时的兼容性和性能影响评估。通过实例展示了内存监控、线程管理和GC监控等功能的实现。
摘要由CSDN通过智能技术生成

什么是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规范提供的所有能力可以在 https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html#architecture 文档中查看,这里简单罗列下一些功能,让我们大概对其能力有个了解。

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 的部分功能
  • 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后 ART虚拟机的部分特性会被禁用 ,需要评估对相关性能的影响点

参考文献

众所周知,Java编译后的Jar包和Class文件,可以轻而易举的使用反编译工具(如JD-GUI)进行反编译,拿到源码。为了保护自己发布的Jar包和Class文件,采用的方式大多是混淆方式,这种方式对于Class文件的加密是不彻底的,还是能够通过分析得出核心算法。本工具是采用jvmti方式对Class文件进行加密,使用C++生成加密和解密库,先用加密库对Jar包进行加密,将加密后的Jar包及解密库文件发布出去,执行时候需要JVM引入解密库文件,解密后执行。c++的.dll文件和.so文件的破解难度是很大的,这就能有效的保护软件和代码的知识产权. 使用方法: 1.打开windows命令行(运行=>cmd=>回车),在命令行中 进入 EncryptJar目录 2.执行 java -jar encrypt.jar 3.输入h,然后回车,可以看到帮助菜单 4.输入3,然后按回车键,进入加入jar文件功能 5.输入要加密的jar文件的路径 6.提示输入秘钥(key)的时候,直接回车,不要输入任何字符(否则后面classhook将不可解密加密后的jar包) 7.输入目标路径(加密后的jar文件路径,此处要注意:jar文件名要保持相同,将加密后的文件保存到不同的目录) 8.将加密后的jar包,替换原来的没有加密的jar包,与要发布的程序一起进行发布.(一般替换lib目录下对应的jar包即可) 9.加密后的jar包运行方法: windows下: 拷贝libClassHook.dll文件到程序的根目录(通常为要执行的jar程序的根目录) 使用以下命令启动程序: java -agentlib:libClassHook -jar xxxxxxxxxxx.jar 则在运行过程中会自动进行解密操作(解密过程是运行过程中用c++的dll进行解密的,可以有效防止破解class文件) 如果执行过程报错,可将程序根目录添加到环境变量path中去 Linux下: 拷贝libClassHook.so到程序的根目录(通常为要执行的jar程序的根目录) 使用以下命令启动程序: java -agentlib:ClassHook -jar xxxxxxxxxxx.jar (这里要删除掉lib,linux系统下会自动补全) 则在运行过程中会自动进行解密操作(解密过程是运行过程中用c++的dll进行解密的,可以有效防止破解class文件) 如果执行过程报错,可以在程序根目录下执行以下语句:export LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH 或将libClassHook.so 拷贝到/usr/lib目录中去。 支持操作系统:加密请在windows64位系统并安装了64位jdk环境下进行。 需要解密运行的程序支持LINUX(64位)和windows(64位)安装了JDK1.8以上的系统。 测试程序: (t_lib目录下的jar包为经过加密的jar包) java -agentlib:libClassHook -jar test.jar
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卓修武

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值