Android内存篇(一)---使用JVMTI监控应用

2022-05-25 09:03:16 浏览数 (1)

前言

一般产品或项目前期都是以快速实现,上线的方式来完成,在生产环境中再开始进行优化,而Android的APP优化,比较重点的还是内存优化,因为每个APP都分配的最大内存,像内存泄露,内存抖动等慢慢都会让APP出来OOM崩溃的情况,最近也是一直在学习和研究内存优化这块,也是在实践中记录笔记。

JVMTI

JVMTI 本质上是在JVM内部的许多事件进行了埋点,通过这些埋点可以给外部提供当前上下文的一些信息。

从 Android 8.0 开始,Android ART已经加入了JVMTI的相关功能。目录位于art/runtime/openjdkjvmti下,从Android.bp可以看到,编译会生成libopenjdkjvmtid.so、libopenjdkjvmti.so文件,其中核心文件是jvmti.h文件,里面定义了一些核心方法和结构体。本地实现时,需要引入该文件来实现对应的Capabilities。

看到.so文件,很明显就是想使用JVMTI,就要用JNI的方式去进行调用了,接下来我们直接从代码上实现。

代码实现

因为要使用JNI,所以项目要创建一个Native C 的项目,完整的Demo源码会在文章最后放出来。

项目目录

01创建Monitor监听类

监听类里面主要就是初始化JVMTI,包括启动和释放,另外加入一个过滤的函数,使用JVMTI监听时,会将所有的对象和方法都列出来,做为线上监听,我们需要写入本地文件里到时可以查看,如果所有的方法都写入,文件会特别大,所以加了一个函数用于只写入我们想要得到的信息。

attachAgent开启JVMTI

代码attachAgent函数是初始化JVMTI的使用,在Android9.0中已将API添加到framework/base/core/java/android/os/Debug.java中,可以直接调用,而Android9.0以下的,需要通过反射的方法进行调用。

JNI方法

定义了三个JNI的方法,用于初始化,释放和过滤要存文件的内容,具体的实现在native-lib.cpp中。

Moniter代码

代码语言:javascript复制
package pers.vaccae.memorymonitor

import android.content.Context
import android.os.Build
import android.os.Debug
import android.util.Log
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.text.SimpleDateFormat
import java.util.*

/**
 * 作者:Vaccae
 * 邮箱:3657447@qq.com
 * 创建时间:15:13
 * 功能模块说明:
 */

object Monitor {

    private const val LIB_NAME = "libmemorymonitor.so"

    fun init(context: Context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            //查找SO的路径
            val libDir: File = File(context.filesDir, "lib")
            if (!libDir.exists()) {
                libDir.mkdirs()
            }
            //判断So库是否存在,不存在复制过来
            val libSo: File = File(libDir, LIB_NAME)
            if (libSo.exists()) libSo.delete()

            val findLibrary =
                ClassLoader::class.java.getDeclaredMethod("findLibrary", String::class.java)
            val libFilePath = findLibrary.invoke(context.classLoader, "memorymonitor") as String
            Log.i("jvmti", "so Path:$libFilePath")

            Files.copy(
                Paths.get(File(libFilePath).absolutePath), Paths.get(
                    libSo.absolutePath
                )
            )


            //加载SO库
            val agentPath = libSo.absolutePath
            System.load(agentPath)

            //agent连接到JVMTI
            attachAgent(agentPath, context.classLoader);

            //开启JVMTI事件监听
            val logDir = File(context.filesDir, "log")
            if (!logDir.exists()) logDir.mkdir()

            //获取当前时间
            val formatter = SimpleDateFormat("yyyyMMddHHmmss")
            val curDate= formatter.format(Date(System.currentTimeMillis()))

            val path = "${logDir.absolutePath}/${curDate}.log"
            attachInit(path)
        } else {
            Log.i("jvmti", "系统版本无法全用JVMTI")
        }
    }

    //agent连接到JVMTI
    private fun attachAgent(agentPath: String, classLoader: ClassLoader) {
        //Android 9.0 
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            Debug.attachJvmtiAgent(agentPath, null, classLoader)
        } else {
            //android 9.0以下版本使用反射方式加载
            val vmDebugClazz = Class.forName("dalvik.system.VMDebug")
            val attachAgentMethod = vmDebugClazz.getMethod("attachAgent", String::class.java)
            attachAgentMethod.isAccessible = true
            attachAgentMethod.invoke(null, agentPath)
        }

    }

    fun release() {
        attachRelease()
    }

    fun writeFilters(pkgname:String){
        Log.i("jvmti",pkgname)
        attachWFilters(pkgname)
    }

    //region JNI函数
    //开启JVMTI事件监听
    private external fun attachInit(path: String)
    private external fun attachRelease()

    private external fun attachWFilters(packagename: String)
    //endregion
}

02拷贝jvmti.h文件

Android的安装目录下有JDK,如果自己安装的JDK,也可以在安装的JDK目录的include下看到jvmti.h的头文件,将这个jvmti.h的头文件拷贝到程序目录cpp下。

当attacchAgent开启监听后,会执行一个回调函数,可以在jvmti.h中看到,我们在C 文件中写这个回调方法的实现用于加载要监听的东西的参数配置

像监听的回调方法,也是在这个头文件中找到,这次我们就监听对象的创建和函数的调用两个方法,如下:

03C nativ-lib中实现回调

在jvmti.h中拷过来后可以看到相关的回调函数了,在native-lib.cpp中主要就是写三个回调方法的实现。

Agent_OnAttach(初始化回调)

objectAlloc(对象创建时的回调)

methodEntry(函数进入时的回调)

JNI attachInit实现初始化的函数

native-lib.cpp完整代码

代码语言:javascript复制
#include <jni.h>
#include <string>
#include <android/log.h>
#include <chrono>
#include "jvmti.h"
#include "MemoryFile.h"

#define LOG_TAG "jvmti"

#define ALOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

jvmtiEnv *mJvmtiEnv;
MemoryFile *memoryFile;
jlong tag = 0;

std::string mPackageName;

//查找过滤
jboolean findFilter(const char *name) {
    std::string tmpstr = name;
    int idx;
    //先判断甩没有Error,有Error直接输出
    idx = tmpstr.find(mPackageName);
    if (idx == std::string::npos) {
        idx = tmpstr.find("OutOfMemoryError");
        if (idx == std::string::npos)//不存在。
        {
            return JNI_FALSE;
        } else {
            return JNI_TRUE;
        }
    } else {
        return JNI_TRUE;
    }
}

// 获取当时系统时间
std::string GetCurrentSystemTime() {
    //auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
    auto now = std::chrono::system_clock::now();
    //通过不同精度获取相差的毫秒数
    uint64_t dis_millseconds =
            std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count()
            - std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count() * 1000;
    time_t tt = std::chrono::system_clock::to_time_t(now);
    struct tm *ptm = localtime(&tt);
    char date[60] = {0};
    sprintf(date, "%d-d-d d:d:d.d",
            (int) ptm->tm_year   1900, (int) ptm->tm_mon   1, (int) ptm->tm_mday,
            (int) ptm->tm_hour, (int) ptm->tm_min, (int) ptm->tm_sec, (int) dis_millseconds);
    return move(std::string(date));
}

jvmtiEnv *CreateJvmtiEnv(JavaVM *vm) {
    jvmtiEnv *jvmti_env;
    jint result = vm->GetEnv((void **) &jvmti_env, JVMTI_VERSION_1_2);
    if (result != JNI_OK) {
        ALOGI("CreateJvmtiEnv is NULL");
        return nullptr;
    }
    return jvmti_env;
}



//调用System.Load()后会回调该方法
extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    ALOGI("JNI_OnLoad");
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    ALOGI("JNI_OnLoad Finish");
    return JNI_VERSION_1_6;
}

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);

    if (mPackageName.empty() || findFilter(classSignature)) {
        //写入日志文件
        char str[500];
        char *format = "%s: object alloc {Thread:%s Class:%s Size:%lld Tag:%lld} rn";
        //ALOGI(format, GetCurrentSystemTime().c_str(),threadInfo.name, classSignature, size, tag);
        sprintf(str, format, GetCurrentSystemTime().c_str(), threadInfo.name, classSignature, size,
                tag);
        memoryFile->write(str, sizeof(char) * strlen(str));
    }
    jvmti_env->Deallocate((unsigned char *) classSignature);
}

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, nullptr);
    //获得方法名字
    jvmti_env->GetMethodName(method, &methodName, nullptr, nullptr);

    if (mPackageName.empty() || findFilter(signature)) {
        //写日志文件
        char str[500];
        char *format = "%s: methodEntry {%s %s} rn";
        //ALOGI(format, GetCurrentSystemTime().c_str(), signature, methodName);
        sprintf(str, format, GetCurrentSystemTime().c_str(), signature, methodName);
        memoryFile->write(str, sizeof(char) * strlen(str));
    }

    jvmti_env->Deallocate((unsigned char *) methodName);
    jvmti_env->Deallocate((unsigned char *) signature);
}

extern "C"
JNIEXPORT void JNICALL
Java_pers_vaccae_memorymonitor_Monitor_attachInit(JNIEnv *env, jobject thiz, jstring path) {
    ALOGI("attachInit");

    const char *_path = env->GetStringUTFChars(path, NULL);

    ALOGI("mPackageName:%s", mPackageName.c_str());

    memoryFile = new MemoryFile(_path);

    //开启JVMTI事件监听
    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(callbacks));
    callbacks.VMObjectAlloc = &objectAlloc;
    callbacks.MethodEntry = &methodEntry;
    
    ALOGI("SetEventCallbacks");
    //设置回调函数
    int error = mJvmtiEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));
    ALOGI("返回码:%dn", error);

    //开启监听
    mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, nullptr);
    mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, nullptr);

    env->ReleaseStringUTFChars(path, _path);

    ALOGI("attachInit Finished");
}

//初始化工作
extern "C"
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM *vm, char *options, void *reserved) {
    int error;
    //准备JVMTI环境
    mJvmtiEnv = CreateJvmtiEnv(vm);

    //开启JVMTI的能力
    jvmtiCapabilities caps;
    mJvmtiEnv->GetPotentialCapabilities(&caps);
    mJvmtiEnv->AddCapabilities(&caps);

    ALOGI("Agent_OnAttach Finish");
    return JNI_OK;
}

extern "C"
JNIEXPORT void JNICALL
Java_pers_vaccae_memorymonitor_Monitor_attachRelease(JNIEnv *env, jobject thiz) {
    delete memoryFile;
    //关闭监听
    mJvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, NULL);
    mJvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_METHOD_ENTRY, NULL);
}

extern "C"
JNIEXPORT void JNICALL
Java_pers_vaccae_memorymonitor_Monitor_attachWFilters(JNIEnv *env, jobject thiz,
                                                      jstring packagename) {
    const char *_packagename = env->GetStringUTFChars(packagename, NULL);
    mPackageName = std::string(_packagename);
    env->ReleaseStringUTFChars(packagename, _packagename);
}

04日志写入文件MemoryFile

建一个MemoryFile的C 类,通过这个类实现消息往MemoryFIle中写入。

MemoryFile.h

代码语言:javascript复制
//
// Created by 36574 on 2022-03-25.
//

#ifndef MEMORYMONITOR_MEMORYFILE_H
#define MEMORYMONITOR_MEMORYFILE_H


class MemoryFile {
private:
    const char* m_path;
    int m_fd;
    int32_t m_size;
    int8_t *m_ptr;
    int m_actualSize;

    void resize(int32_t needSize);

public:
    MemoryFile(const char *path);

    ~MemoryFile();

    void write(char *data, int dataLen);
};


#endif //MEMORYMONITOR_MEMORYFILE_H

MemoryFile.cpp

代码语言:javascript复制
//
// Created by 36574 on 2022-03-25.
//

#include <cstdint>
#include <unistd.h>
#include <cstring>
#include <fcntl.h>
#include <sys/mman.h>
#include <mutex>
#include "MemoryFile.h"

std::mutex mtx;

//系统给我们提供真正的内存时,用页为单位提供
//内存分页大小 一分页的大小
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<int8_t *>(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) {
    mtx.lock();
    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;
    mtx.unlock();
}

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<int8_t *>(mmap(0,m_size,PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0));

}

05CMakeList中加入MemoryFile.cpp

加入了文件写入的类,所以要在CMakeList中加入进来这个cpp

06写一个OOM的操作实现效果

定义一个Byte数组,直接就是1G,肯定会OOM

在MainActivity中初始化这个类

自己定义的Application中OnCreate直接初始化JVMTI监听,并且只留下含有vaccae的信息和错误信息。

实现效果

设备的data/data/包名/files下面现在是空的,我们直接运行程序

可以看到,一运行就直接OutOfMemoryError了

重新刷新data/data/包名/files/log下有一个当前时间的log文件,把它导出到电脑上

打开log文件后可以看到,OutOfMemoryError处上方,执行的是ByteTest中的init方法,也就是我们代码中MainActivity的OnCreate是ByteTest()。这样就可以定位的错误的位置了。

重点

上面的真机用的是android9.0以后的,所以没有问题,代码中也写了8.0用的反射方法,我也专门创建了android8.1的虚拟机,发现上面的方式并不能用,下一篇就专门针对android8.1怎么实现讲解。

源码地址

https://github.com/Vaccae/AndroidJVMTIDemo.git

0 人点赞