Android JVMTI实现应用内存动态检测

一.前言

       在平常的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的工作。
       总结一下调用流程:

attach Agent.png

四.案例分析

       本案例实现了对应用内部对象创建及释放、方法进入及退出事件的监听。
       由于需要将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),两者最后调用的都是同一个方法,执行流程如下:

load lib.png

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的使用过程及对对象创建释放、方法进入退出事件的监听,最后对事件信息存文件,这样当应用因为内存使用不当导致的问题,通过文件就可以分析出来。

你可能感兴趣的:(Android JVMTI实现应用内存动态检测)