前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android内存篇(一)---使用JVMTI监控应用

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

作者头像
Vaccae
发布2022-05-25 09:03:16
2K0
发布2022-05-25 09:03:16
举报
文章被收录于专栏:微卡智享

前言

一般产品或项目前期都是以快速实现,上线的方式来完成,在生产环境中再开始进行优化,而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-%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

代码语言: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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 微卡智享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • JVMTI
  • 代码实现
    • 项目目录
      • 01创建Monitor监听类
    • Moniter代码
      • 02拷贝jvmti.h文件
      • 03C++ nativ-lib中实现回调
    • native-lib.cpp完整代码
      • 04日志写入文件MemoryFile
    • MemoryFile.h
      • MemoryFile.cpp
        • 05CMakeList中加入MemoryFile.cpp
        • 06写一个OOM的操作实现效果
      • 实现效果
        • 重点
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档