什么是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虚拟机),它增加了一些限制如(摘自官网说明):
- 首先,提供代理接口 JVMTI 的代码作为运行时插件(而不是运行时的核心组件)来实现。插件加载可能会受到限制,这样可阻止代理找到任何接口点
- 其次,
ActivityManager
类和运行时进程只允许代理连接到可调试的应用。可调试应用由其开发者签核,以供分析和插桩,而不会分发给最终用户。Google Play 商店不允许发布可调试应用。这可确保普通应用(包括核心组件)无法遭到检测或操纵。
在Android 中 JVMTI 和Agent的架构如图
在Android中 使用JVMTI有两种方式
- 虚拟机启动时连接代理
- 运行时。将代理加载到当前进程中
关于两种方式的具体使用流程亦可参照官网介绍,这里不做详述。其jvmti接口的具体实现可以在这里找到。其最终被编译成 libopenjdkjvmti.so。 以我的64位手机为例,在 目录 /lib64/system/libopenjdkjvmti.so 可以找到该动态库
JVMTI 的一些应用实例
这里简单介绍写 JVMTI 的一些应用。
以Android官方为例
- 基于JVMTI (重定义类功能)实现 Apply Changes功能
- 这部分 agent 实现可以在这里找到,从这里你可以了解基于jvmti实现热加载的核心逻辑
- Android Studio Profiler 的部分功能
- 内存分配监控
- GC监控
- 通过替换dex 中的class 实现对系统类的各种监控 (ClassTransform)
- JVMTI 是JDI 体系的后端实现,我们可以自己基于JVMTI实现程序调试能力,比如美团有一篇文章介绍了基于JDWP 实现了线上调试的功能,关于JDPA体系可以参考oracle文档 ,以下是 JVMTI、JDWP、JDI的一个关系结构图
这里想到了另外一些思路,如果可以突破 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.
在源码中搜索上述日志,我们找到了相应代码
上述日志的输出执行时机点是在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中
继续跟进 IsFullJvmtiAvailable 的实现
原来在 源码中 获取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包一致
这里我只监控了GC事件、线程创建销毁、对象分配这三类事件,其他事件是否能正常使用未经过充分测试,从原理上讲,JVMTI的实现和大部分系统、虚拟机提供监控实现机制差不多,都是通过在源码中事先写入相关监控代码实现的,只要监控点的代码是依赖于Dbg和Runtime的那两个变量那么都应该能正常工作。
线上 使用 JVMTI进行性能监控的评估点
上节中,我们实现了在 生产环境使用 JVMTI的能力,然而这离将JVMTI真正运用在线上生产环境还有很多需要考虑的点如:
- 兼容性
- 只能在8.0及以上系统使用
- 依赖于 dlsym 调用系统函数,不排查Android官方或者第三方厂商对这些函数进行了修改 需要测试兼容性
- 性能
- 开启JVMTI后 ART虚拟机的部分特性会被禁用 ,需要评估对相关性能的影响点