一.前言
在平常的Android应用开发中,经常会遇到应用因内存问题导致的异常,可能大家第一反应是:分析log及堆栈信息;但是我们知道堆栈信息只是最后的结果表现而已,真正出问题的地方或原因是之前由于不正常的内存操作,导致内存一直占用没有被释放,出现内存泄露,最后OOM。
为了解决上述问题,最直接有效的方式是:动态内存分配监听
记录程序执行过程中的动态内存分配,当发生OOM时,就能够分析记录信息掌握内存使用情况;如:是否存在内存泄露、内存抖动等问题。
接下来就是主角登场了----JVMTI。
二.JVMTI
a.简介
Java虚拟机工具接口,是Java虚拟机提供的一整套后门,通过这套后门可以对虚拟机方方面面进行监控,它可以监控jvm内部事件的执行,包括内存申请、线程创建、类加载、GC信息、方法执行等,也可以控制JVM的某些行为。具体可以参考oracle 的文档:JVM Tool Interface
JVMTI 本质上是在JVM内部的许多事件进行了埋点,通过这些埋点可以给外部提供当前上下文的一些信息,甚至可以接受外部的命令来改变下一步的动作。
外部程序一般利用C/C++实现一个JVMTI Agent,JVMTI Agent的启动需要虚拟机的支持,在Agent里面注册一些JVM事件的回调。当事件发生时JVMTI调用这些回调方法。Agent可以在回调方法里面实现自己的逻辑。
JVMTI Agent是以动态链接库的形式被虚拟机加载的, Agent和虚拟机运行在同一个进程中,虚拟机通过dlopen打开Agent动态链接库。
b.功能
一些重要的功能包括:
1.重新定义类
2.跟踪对象分配和垃圾回收过程
3.遵循对象的引用树,遍历堆中的所有对象
4.检查 Java 调用堆栈
5.暂停(和恢复)所有线程
不同版本的 Android 可能会提供不同的功能
c.兼容性
此功能需要仅针对 Android 8.0 及更高版本提供的核心运行时支持,设备制造商无需进行任何更改即可实现此功能,它是 AOSP 的一部分。
从 Android 8.0 开始,Android ART已经加入了JVMTI的相关功能。目录位于art/runtime/openjdkjvmti下,从Android.bp可以看到,编译会生成libopenjdkjvmtid.so、libopenjdkjvmti.so文件,其中核心文件是jvmti.h文件,里面定义了一些核心方法和结构体。本地实现时,需要引入该文件来实现对应的Capabilities。
d.API调用
在 Android 9.0,已将API添加到framework/base/core/java/android/os/Debug.java中,对应API如下:
/**
* 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 SecurityException if the app is not debuggable.
*/
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);
}
}
接下来一起分析一下当app调用完attachJvmtiAgent()后,源码的执行流程,本文以Android 8.1作为源码进行分析。
三.源码分析
通过上面的分析可以看到,在attachJvmtiAgent()内部,会调用VMDebug类内部的attachAgent()方法,由于attachJvmtiAgent()是在Android 9.0才加入的,那么在Android 8.1平台只能通过反射来执行,直接反射VMDebug的attachAgent()方法。
a.VMDebug.java
该类路径:libcore/dalvik/src/main/java/dalvik/system/VMDebug.java
/**
* Attaches an agent to the VM.
*
* @param agent The path to the agent .so file plus optional agent arguments.
*/
public static native void attachAgent(String agent) throws IOException;
从上面可以看到,该类是native方法,会通过Jni调用到native层对应的实现方法,该实现在dalvik_system_VMDebug.cc内部。
b.dalvik_system_VMDebug.cc
该类路径:art/runtime/native/dalvik_system_VMDebug.cc
static void VMDebug_attachAgent(JNIEnv* env, jclass, jstring agent) {
if (agent == nullptr) {
ScopedObjectAccess soa(env);
ThrowNullPointerException("agent is null");
return;
}
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(filename);
}
在VMDebug_attachAgent()内部首先判断传入so的路径是否为空,然后判断是否为debug模式,以上两个条件都满足[注意jvmti只适应于debug版本],最后会调用AttachAgent()方法,该方法实现是在runtime.cc内部。
c.runtime.cc
该类路径:art/runtime/runtime.cc
// Attach a new agent and add it to the list of runtime agents
//
// TODO: once we decide on the threading model for agents,
// revisit this and make sure we're doing this on the right thread
// (and we synchronize access to any shared data structures like "agents_")
//
void Runtime::AttachAgent(const std::string& agent_arg) {
std::string error_msg;
if (!EnsureJvmtiPlugin(this, &plugins_, &error_msg)) {
LOG(WARNING) << "Could not load plugin: " << error_msg;
ScopedObjectAccess soa(Thread::Current());
ThrowIOException("%s", error_msg.c_str());
return;
}
ti::Agent agent(agent_arg);
int res = 0;
ti::Agent::LoadError result = agent.Attach(&res, &error_msg);
if (result == ti::Agent::kNoError) {
agents_.push_back(std::move(agent));
} else {
LOG(WARNING) << "Agent attach failed (result=" << result << ") : " << error_msg;
ScopedObjectAccess soa(Thread::Current());
ThrowIOException("%s", error_msg.c_str());
}
}
在AttachAgent()内部,会根据传入参数来创建Agent,然后执行Attach()方法,该方法是在agent.h内部。
d.agent.h
该类路径:art/runtime/ti/agent.h
LoadError Attach(/*out*/jint* call_res, /*out*/std::string* error_msg) {
VLOG(agents) << "Attaching agent: " << name_ << " " << args_;
return DoLoadHelper(true, call_res, error_msg);
}
bool IsStarted() const {
return dlopen_handle_ != nullptr;
}
在Attach()内部会调用DoLoadHelper(),该方法位于agent.cc内部。
e.agent.cc
该类路径:art/runtime/ti/agent.cc
Agent::LoadError Agent::DoLoadHelper(bool attaching,
/*out*/jint* call_res,
/*out*/std::string* error_msg) {
......
//如果打开过,就不会再打开了,IsStarted()方法在agent.h方法内部判断
if (IsStarted()) {
*error_msg = StringPrintf("the agent at %s has already been started!", name_.c_str());
VLOG(agents) << "err: " << *error_msg;
return kAlreadyStarted;
}
//调用DoDlOpen()
LoadError err = DoDlOpen(error_msg);
if (err != kNoError) {
VLOG(agents) << "err: " << *error_msg;
return err;
}
AgentOnLoadFunction callback = attaching ? onattach_ : onload_;
if (callback == nullptr) {
*error_msg = StringPrintf("Unable to start agent %s: No %s callback found",
(attaching ? "attach" : "load"),
name_.c_str());
VLOG(agents) << "err: " << *error_msg;
return kLoadingError;
}
// Need to let the function fiddle with the array.
std::unique_ptr copied_args(new char[args_.size() + 1]);
strlcpy(copied_args.get(), args_.c_str(), args_.size() + 1);
//回调加载的本地库内部的Agent_OnAttach()方法
*call_res = callback(Runtime::Current()->GetJavaVM(),
copied_args.get(),
nullptr);
if (*call_res != 0) {
*error_msg = StringPrintf("Initialization of %s returned non-zero value of %d",
name_.c_str(), *call_res);
VLOG(agents) << "err: " << *error_msg;
return kInitializationError;
} else {
return kNoError;
}
}
内部调用方法DoDlOpen():
Agent::LoadError Agent::DoDlOpen(/*out*/std::string* error_msg) {
DCHECK(error_msg != nullptr);
DCHECK(dlopen_handle_ == nullptr);
DCHECK(onload_ == nullptr);
DCHECK(onattach_ == nullptr);
DCHECK(onunload_ == nullptr);
//调用dlopen()
dlopen_handle_ = dlopen(name_.c_str(), RTLD_LAZY);
if (dlopen_handle_ == nullptr) {
*error_msg = StringPrintf("Unable to dlopen %s: %s", name_.c_str(), dlerror());
return kLoadingError;
}
//通过FindSymbol来从加载的库中寻找Agent_xx方法
onload_ = reinterpret_cast(FindSymbol(AGENT_ON_LOAD_FUNCTION_NAME));
if (onload_ == nullptr) {
VLOG(agents) << "Unable to find 'Agent_OnLoad' symbol in " << this;
}
onattach_ = reinterpret_cast(FindSymbol(AGENT_ON_ATTACH_FUNCTION_NAME));
if (onattach_ == nullptr) {
VLOG(agents) << "Unable to find 'Agent_OnAttach' symbol in " << this;
}
onunload_= reinterpret_cast(FindSymbol(AGENT_ON_UNLOAD_FUNCTION_NAME));
if (onunload_ == nullptr) {
VLOG(agents) << "Unable to find 'Agent_OnUnload' symbol in " << this;
}
return kNoError;
}
内部调用方法FindSymbol():
void* Agent::FindSymbol(const std::string& name) const {
CHECK(IsStarted()) << "Cannot find symbols in an unloaded agent library " << this;
return dlsym(dlopen_handle_, name.c_str());
}
通过以上调用关系可以看到,当我们加载完本地so后,然后调用Debug.attachJvmtiAgent()[Android 9.0]或反射调用VMDebug.attachAgent()[Android 8.1],会回调so内部的Agent_XX方法,本地测试发现,会回调Agent_OnAttach()方法,那我们就在Agent_OnAttach()内部来初始化Jvmti的工作。
总结一下调用流程:
四.案例分析
本案例实现了对应用内部对象创建及释放、方法进入及退出事件的监听。
由于需要将so作为agent进行attach,所以涉及到jni编程,生成so,关于jni编程,可以参考之前的一篇文章AndroidStudio 来编写jni及生成so。本文就略过了,直接上代码。
a.Monitor.java
public class Monitor {
private static final String LIB_NAME = "monitor_agent";
public static void init(Context application) {
//最低支持Android 8.0
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
//获取so的地址后加载
String agentPath = getAgentLibPath(application);
System.load(agentPath);
//加载jvmti
attachAgent(agentPath, application.getClassLoader());
//开启jvmti事件监听
agent_init(root.getAbsolutePath());
}
private static void attachAgent(String agentPath, ClassLoader classLoader) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Debug.attachJvmtiAgent(agentPath, null, classLoader);
} else {
Class vmDebugClazz = Class.forName("dalvik.system.VMDebug");
Method attachAgentMethod = vmDebugClazz.getMethod("attachAgent", String.class);
attachAgentMethod.setAccessible(true);
attachAgentMethod.invoke(null, agentPath);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
private static String getAgentLibPath(Context context) {
try {
ClassLoader classLoader = context.getClassLoader();
Method findLibrary = ClassLoader.class.getDeclaredMethod("findLibrary", String.class);
//so的地址
String jvmtiAgentLibPath = (String) findLibrary.invoke(classLoader, LIB_NAME);
//将so拷贝到程序私有目录 /data/data/packageName/files/monitor/agent.so
File filesDir = context.getFilesDir();
File jvmtilibDir = new File(filesDir, "monitor");
if (!jvmtilibDir.exists()) {
jvmtilibDir.mkdirs();
}
File agentLibSo = new File(jvmtilibDir, "agent.so");
if (agentLibSo.exists()) {
agentLibSo.delete();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Files.copy(Paths.get(new File(jvmtiAgentLibPath).getAbsolutePath()), Paths.get((agentLibSo).getAbsolutePath()));
}
return agentLibSo.getAbsolutePath();
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
public static void release() {
agent_release();
}
private native static void agent_init();
private native static void agent_release();
}
1.application内部通过init()来进行初始化,包括加载库(后面补充一下so加载流程)、创建存放日志的目录;
2.通过getAgentLibPath()来获取到so的路径;
3.在attachAgent()内部直接或通过反射来attachAgent();
4.native方法agent_init()及agent_release()方法来开启及暂停jvmti。
so的加载会调用System.load(path)或System.loadLibrary(name),两者最后调用的都是同一个方法,执行流程如下:
b.agentlib.cpp
#include
#include
#include "jvmti.h"
#include "utils.h"
jvmtiEnv *mJvmtiEnv = NULL;
jlong tag = 0;
//初始化工作
extern "C"
JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options, void *reserved) {
//准备jvmti环境
vm->GetEnv(reinterpret_cast(&mJvmtiEnv), JVMTI_VERSION_1_2);
//开启jvmti的能力
jvmtiCapabilities caps;
//获取所有的能力
mJvmtiEnv->GetPotentialCapabilities(&caps);
mJvmtiEnv->AddCapabilities(&caps);
return JNI_OK;
}
//调用System.Load()后会回调该方法
extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
void JNICALL objectAlloc(jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread,
jobject object, jclass object_klass, jlong size) {
//对象创建
}
void JNICALL objectFree(jvmtiEnv *jvmti_env, jlong tag) {
//对象释放
}
void JNICALL methodEntry(jvmtiEnv *jvmti_env,JNIEnv* jni_env,jthread thread,jmethodID method) {
//方法进入
}
void JNICALL methodExit(jvmtiEnv *jvmti_env,JNIEnv* jni_env,jthread thread,jmethodID method,jboolean was_popped_by_exception,
jvalue return_value) {
//方法退出
}
extern "C"
JNIEXPORT void JNICALL
Java_com_hly_memorymonitor_Monitor_agent_1init(JNIEnv *env, jclass jclazz) {
//开启jvm事件监听
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.MethodEntry = &methodEntry;
callbacks.MethodExit = &methodExit;
callbacks.VMObjectAlloc = &objectAlloc;
callbacks.ObjectFree = &objectFree;
//设置回调函数
mJvmtiEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));
//开启监听
mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, NULL);
mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_OBJECT_FREE, NULL);
mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, NULL);
mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_EXIT, NULL);
env->ReleaseStringUTFChars(_path, path);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_hly_memorymonitor_Monitor_agent_1release(JNIEnv *env, jclass clazz) {
mJvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, NULL);
mJvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_OBJECT_FREE, NULL);
mJvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_METHOD_ENTRY, NULL);
mJvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_METHOD_EXIT, NULL);
}
1.在Agent_OnAttach()内部初始化,准备jvmti环境,开启及获取能力;
2.在xx_agent_1init()内部,开启jvmti事件监听,设置需要关注的回调(该回调在jvmti.h内部有详细的定义,设置需要关注的即可,本案例关注了JVMTI_EVENT_VM_OBJECT_ALLOC、JVMTI_EVENT_OBJECT_FREE、JVMTI_EVENT_OBJECT_FREE、JVMTI_EVENT_METHOD_EXIT),执行SetEventNotificationMode JVMTI_ENABLE 开启监听;
3.在xx_agent_1release()内部,执行SetEventNotificationMode JVMTI_DISABLE 关闭监听;
c.获取事件信息
在对指定的事件监听之后,需要提取到需要的信息,比如:创建了什么对象、释放了什么对象、进入了哪个方法、退出了哪个方法等等。
附加一下获取信息的方法:
void JNICALL objectAlloc(jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread,
jobject object, jclass object_klass, jlong size) {
//给对象打tag,后续在objectFree()内可以通过该tag来判断是否成对出现释放
tag += 1;
jvmti_env->SetTag(object, tag);
//获取线程信息
jvmtiThreadInfo threadInfo;
jvmti_env->GetThreadInfo(thread, &threadInfo);
//获得 创建的对象的类签名
char *classSignature;
jvmti_env->GetClassSignature(object_klass, &classSignature, nullptr);
//获得堆栈信息
char *stackInfo = createStackInfo(jvmti_env, jni_env, thread, 10);
ALOGE("object alloc, Thread is %s, class is %s, size is %s, tag is %lld, stackInfo is %s", threadInfo.name, classSignature, size, tag, stackInfo);
}
void JNICALL methodEntry(jvmtiEnv *jvmti_env,JNIEnv* jni_env,jthread thread,jmethodID method) {
jclass clazz;
char *signature;
char *methodName;
//获得方法对应的类
jvmti_env->GetMethodDeclaringClass(method, &clazz);
//获得类的签名
jvmti_env->GetClassSignature(clazz, &signature, 0);
//获得方法名字
jvmti_env->GetMethodName(method, &methodName, NULL, NULL);
ALOGE("methodEntry method name is %s", methodName);
jvmti_env->Deallocate((unsigned char *)methodName);
jvmti_env->Deallocate((unsigned char *)signature);
}
d.存文件
为了效率性,可以通过mmap来实现文件的写入,代码如下:
#include
#include
#include
#include
#include
#include "MemoryFile.h"
//系统给我们提供真正的内存时,用页为单位提供
//内存分页大小 一分页的大小
int32_t DEFAULT_FILE_SIZE = getpagesize();
MemoryFile::MemoryFile(const char *path) {
m_path = path;
m_fd = open(m_path, O_RDWR | O_CREAT, S_IRWXU);
m_size = DEFAULT_FILE_SIZE;
//将文件设置为m_size大小
ftruncate(m_fd, m_size);
//mmap内存映射
m_ptr = static_cast(mmap(0, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0));
//初始化m_actualSize为0
m_actualSize = 0;
}
MemoryFile::~MemoryFile() {
munmap(m_ptr, m_size);
close(m_fd);
}
void MemoryFile::write(char *data, int dataLen) {
if (m_actualSize + dataLen >= m_size) {
resize(m_actualSize + dataLen);
}
//将data的dataLen长度的数据 拷贝到 m_ptr + m_actualSize;
memcpy(m_ptr + m_actualSize, data, dataLen);//操作内存,通过内存映射就写入文件了
//重新设置最初位置
m_actualSize += dataLen;
}
void MemoryFile::resize(int32_t needSize) {
int32_t oldSize = m_size;
do {
m_size *= 2;
} while (m_size < needSize);
//设置文件大小
ftruncate(m_fd, m_size);
//解除映射
munmap(m_ptr, oldSize);
//重新进行mmap内存映射
m_ptr = static_cast(mmap(0, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0));
}
本文介绍了jvmti的使用过程及对对象创建释放、方法进入退出事件的监听,最后对事件信息存文件,这样当应用因为内存使用不当导致的问题,通过文件就可以分析出来。