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

学更好的别人,

做更好的自己。

——《微卡智享》

Android内存篇(一)---使用JVMTI监控应用_第1张图片

本文长度为4869,预计阅读10分钟

前言

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

61cf06b39505d00942f900c1194b0548.png

JVMTI

2584114d7553fcc6b1ce36327cbceddf.png

微卡智享

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

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

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

代码实现

d9901c0181e11b6e180a8c7b44fde9fa.png

微卡智享

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

微卡智享

项目目录

Android内存篇(一)---使用JVMTI监控应用_第2张图片

01

创建Monitor监听类

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

attachAgent开启JVMTI

Android内存篇(一)---使用JVMTI监控应用_第3张图片

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

JNI方法

Android内存篇(一)---使用JVMTI监控应用_第4张图片

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

Moniter代码

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
 * 邮箱:[email protected]
 * 创建时间: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内存篇(一)---使用JVMTI监控应用_第5张图片

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

Android内存篇(一)---使用JVMTI监控应用_第6张图片

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

Android内存篇(一)---使用JVMTI监控应用_第7张图片

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

Android内存篇(一)---使用JVMTI监控应用_第8张图片

Android内存篇(一)---使用JVMTI监控应用_第9张图片

03

C++ nativ-lib中实现回调

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

Agent_OnAttach(初始化回调)

Android内存篇(一)---使用JVMTI监控应用_第10张图片

objectAlloc(对象创建时的回调)

Android内存篇(一)---使用JVMTI监控应用_第11张图片

methodEntry(函数进入时的回调)

Android内存篇(一)---使用JVMTI监控应用_第12张图片

JNI attachInit实现初始化的函数

Android内存篇(一)---使用JVMTI监控应用_第13张图片

native-lib.cpp完整代码

#include 
#include 
#include 
#include 
#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(now.time_since_epoch()).count()
            - std::chrono::duration_cast(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-%02d-%02d %02d:%02d:%02d.%03d",
            (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} \r\n";
        //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} \r\n";
        //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("返回码:%d\n", 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

//
// 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

//
// Created by 36574 on 2022-03-25.
//


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


}

05

CMakeList中加入MemoryFile.cpp

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

Android内存篇(一)---使用JVMTI监控应用_第14张图片

06

写一个OOM的操作实现效果

Android内存篇(一)---使用JVMTI监控应用_第15张图片

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

Android内存篇(一)---使用JVMTI监控应用_第16张图片

在MainActivity中初始化这个类

Android内存篇(一)---使用JVMTI监控应用_第17张图片

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

实现效果

Android内存篇(一)---使用JVMTI监控应用_第18张图片

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

Android内存篇(一)---使用JVMTI监控应用_第19张图片

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

Android内存篇(一)---使用JVMTI监控应用_第20张图片

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

Android内存篇(一)---使用JVMTI监控应用_第21张图片

Android内存篇(一)---使用JVMTI监控应用_第22张图片

Android内存篇(一)---使用JVMTI监控应用_第23张图片

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

微卡智享

重点

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

源码地址

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

点击原文链接可以看到“码云”的源码地址

ca983b6ed3e85f38827725ba16564d15.png

9798edf62f295e96ff052e29f9e0d5ab.png

往期精彩回顾

 

Android内存篇(一)---使用JVMTI监控应用_第24张图片

Android中关于OOM的捕获的方法

 

 

Android内存篇(一)---使用JVMTI监控应用_第25张图片

使用开源SiteServer替代某老牌CMS做网站

 

 

Android内存篇(一)---使用JVMTI监控应用_第26张图片

Android JetPack--拖拽DragAndDrop使用及和旧版对比

 

你可能感兴趣的:(android,java,python,c++,linux)