返回顶部
首页 > 资讯 > 移动开发 >Android性能优化之JVMTI与内存分配
  • 337
分享到

Android性能优化之JVMTI与内存分配

2024-04-02 19:04:59 337人浏览 独家记忆
摘要

目录前言JVMTIJVMTI 简介:native层开启jvmti前置准备复写Agent开启jvmtiCapabilities设置jvmtiEventCallbacks开启监听java

前言

内存治理一直是每个开发者最关心的问题,我们在日常开发中会遇到各种各样的内存问题,比如OOM,内存泄露,内存抖动等等,这些问题都有以下共性:

  • 难发现,内存问题一般很难发现,业务开发中关系系数更少
  • 治理困难,内存问题治理困难,比如oom,往往堆栈只是压死骆驼的最后一根稻草
  • 易复发,几乎没有一种方案,能够杜绝内存问题,比如内存泄露几乎是100%存在,只是不同项目影响的范围不同而已

内存问题目前经过业内多年沉淀以及开发,已经有很多方案了,比如检查内存泄露(LeakCanary,MIT,KOOM等)。相关文章已经有很多,所以我们从另一个角度出发,虚拟机侧有没有想过的方案检测内存呢?有的,那就是JVMTI(Java Virtual Machine Tool Interface)即指 Java 虚拟机工具接口,它是一套由虚拟机直接提供的 native 接口,我们可以从这里面获取虚拟机运行时的大部分信息。

友情提示:本文涉及native c层的代码,如果读者不熟悉也没关系,已经尽量减少相关的代码阅读成本啦!冲就对啦!JVMTI在debug模式下有很多用处,当然release环境也可以通过hook方式开启,但是不太建议,虽然jvmti有诸多限制,但是不妨碍我们多了解一个“黑科技”

JVMTI

JVMTI 简介:

JVMTI,即由java虚拟机提供的面向虚拟机接口的一套监控api,虽然虚拟机中一直存在,但是在Android中是在Android 8.0(API 级别 26)或更高版本的设备上才正式支持。jvmti的功能本质就是“埋点化”,把jvm的一些事件通过“监听”的方式暴露给外部开发调试

jvmti监听的事件包包含了虚拟机中线程、内存、堆、栈、类、方法、变量,事件、定时器,等创建销毁相关事件,本次我们从实战的角度出发,看看如何实现一次内存分配的监听

native层开启jvmti

前置准备

使用jvmti之前,我们需要创建一个native工程,同时我们需要使用jvmti的api,在native中就是头文件了,我们需要复制一份jdk中的名叫jvmti.h的头文件(在我们安装的jdk/include目录下),到我们的项目cpp根目录即可

此时我们也自定义一个memory.cpp作为我们使用jvmti的函数载体。jvmti.h里面包含了我们所需要的一切函数定义与常量,当然,这个头文件并不需要随着native工程进行打包,因为在真正使用到jvmti相关的工具时,是由系统进行so依赖查找进行定位的,该so位于系统库中(libopenjdkjvmtid.so、libopenjdkjvmti.so),所以我们不用关心具体的实现,接下来我们按照步骤进行即可,包括native层与java层

复写Agent

作为第一步,我们需要复写jvmti.h中的

JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);

这个是jvmti中的agent初始化的时候,由native回调,在这里我们可以拿到JavaVM环境,同时可以创建jvmtiEnv对象,该对象非常重要,用于native进行接下来的各种监听处理

// 全局的jvmti环境变量
jvmtiEnv *mJvmtiEnv;
extern "C"
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM *vm, char *options, void *reserved) {
    //准备JVMTI环境,初始化mJvmtiEnv
    vm->GetEnv((void **) &mJvmtiEnv, JVMTI_VERSION_1_2);
    return JNI_OK;
}

开启jvmtiCapabilities

默认时,jvmti中是不提供任何能力给我们使用的,我们可以通过jvmtiEnv,去查询当前虚拟机实现的哪几种jvmti回调

jvmtiError GetPotentialCapabilities(jvmtiCapabilities* capabilities_ptr) {
  return functions->GetPotentialCapabilities(this, capabilities_ptr);
}
jvmtiError AddCapabilities(const jvmtiCapabilities* capabilities_ptr) {
  return functions->AddCapabilities(this, capabilities_ptr);
}

可以看到,我们只需要传入一个jvmtiCapabilities对象指针即可,之后的能力数据就会被填充到该对象,所以我们接下来在Agent_OnAttach函数中继续补充以下代码

//初始化工作
extern "C"
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM *vm, char *options, void *reserved) {
    //准备JVMTI环境,初始化mJvmtiEnv
    vm->GetEnv((void **) &mJvmtiEnv, JVMTI_VERSION_1_2);
    //开启JVMTI的能力:到这一步啦!!
    jvmtiCapabilities caps;
    mJvmtiEnv->GetPotentialCapabilities(&caps);
    mJvmtiEnv->AddCapabilities(&caps);
    __android_log_print(ANDROID_LOG_ERROR, "hello", "Agent_OnAttach");
    return JNI_OK;
}

设置jvmtiEventCallbacks

我们已经查询到了jvmti所支持的回调,这个时候就到了正式设置回调的环节,jvmti中支持以下几种回调类型

typedef struct {
                              
    jvmtiEventVMInit VMInit;
                              
    jvmtiEventVMDeath VMDeath;
                              
    jvmtiEventThreadStart ThreadStart;
                              
    jvmtiEventThreadEnd ThreadEnd;
                              
    jvmtiEventClassFileLoadHook ClassFileLoadHook;
                              
    jvmtiEventClassLoad ClassLoad;
                              
    jvmtiEventClassPrepare ClassPrepare;
                              
    jvmtiEventVMStart VMStart;
                              
    jvmtiEventException Exception;
                              
    jvmtiEventExceptionCatch ExceptionCatch;
                              
    jvmtiEventSingleStep SingleStep;
                              
    jvmtiEventFramePop FramePop;
                              
    jvmtiEventBreakpoint Breakpoint;
                              
    jvmtiEventFieldAccess FieldAccess;
                              
    jvmtiEventFieldModification FieldModification;
                              
    jvmtiEventMethodEntry MethodEntry;
                              
    jvmtiEventMethodExit MethodExit;
                              
    jvmtiEventNativeMethodBind NativeMethodBind;
                              
    jvmtiEventCompiledMethodLoad CompiledMethodLoad;
                              
    jvmtiEventCompiledMethodUnload CompiledMethodUnload;
                              
    jvmtiEventDynamicCodeGenerated DynamicCodeGenerated;
                              
    jvmtiEventDataDumpRequest DataDumpRequest;
                              
    jvmtiEventReserved reserved72;
                              
    jvmtiEventMonitorWait MonitorWait;
                              
    jvmtiEventMonitorWaited MonitorWaited;
                              
    jvmtiEventMonitorContendedEnter MonitorContendedEnter;
                              
    jvmtiEventMonitorContendedEntered MonitorContendedEntered;
                              
    jvmtiEventReserved reserved77;
                              
    jvmtiEventReserved reserved78;
                              
    jvmtiEventReserved reserved79;
                              
    jvmtiEventResourceExhausted ResourceExhausted;
                              
    jvmtiEventGarbageCollectionStart GarbageCollectionStart;
                              
    jvmtiEventGarbageCollectionFinish GarbageCollectionFinish;
                              
    jvmtiEventObjectFree ObjectFree;
                              
    jvmtiEventVMObjectAlloc VMObjectAlloc;
} jvmtiEventCallbacks;

我们需要监听的是内存分配与销毁的监听即可,分别是VMObjectAlloc与ObjectFree,在jvmtiEventCallbacks设定我们想要监听的事件之后,我们可以通过jvmtiEnv->SetEventCallbacks方法设定即可,所以我们可以继续在Agent_OnAttach中补充以下代码

jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.VMObjectAlloc = &objectAlloc;
callbacks.ObjectFree = &objectFree;
//设置回调函数
mJvmtiEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));

其中objectAlloc是我们自定义的监听处理函数,如果jvm执行内存分配事件,就会回调此函数,该函数定义是

typedef void (JNICALL *jvmtiEventVMObjectAlloc)
    (jvmtiEnv *jvmti_env,
     JNIEnv* jni_env,
     jthread thread,
     jobject object,
     jclass object_klass,
     jlong size);

所以我们自定义的回调函数也要根据此定义进行编写。因为这里会回调所有java层的对象创建事件,回调次数非常多,在实际中我们可能并不关心系统类是如何分配内存的,而是关心我们自己的项目中的类的内存情况,所以这里我们做一个过滤,只有是项目的类我们才进行记录

void JNICALL objectAlloc(jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread,
                         jobject object, jclass object_klass, jlong size) {
    jvmti_env->SetTag(object, tag);
    tag+= 1;
    char *classSignature;
    // 获取类签名
    jvmti_env->GetClassSignature(object_klass, &classSignature, nullptr);
    // 过滤条件
    if(strstr(classSignature, "com/test/memory") != nullptr){
        __android_log_print(ANDROID_LOG_ERROR, "hello", "%s",classSignature);
        myVM->AttachCurrentThread( &currentEnv, nullptr);
        // 这个list我们之后解释
        list.push_back(tag);
        char str[500];
        char *fORMat = "%s: object alloc {Tag:%lld} \r\n";
        sprintf(str, format, classSignature,
                tag);
        memoryFile->write(str, sizeof(char) * strlen(str));
    }
    jvmti_env->Deallocate((unsigned char *) classSignature);
}

我们可以看到,我们在中间做了一个jvmti_env->SetTag的操作,这个是给这个分配的对象进行了一个打标签的动作(我们需要观察该对象是否被销毁,所以需要一个唯一标识符),我们会在释放的时候用到。因为回调的操作可能会有很多,我们采用普通的io必定会导致native层的阻塞,所以这里就要靠我们的mmap登场了,通过mmap我们可以高效的处理频繁的io,mmap不熟悉的可以看这篇,memoryFile->write是一个通过mmap的写文件操作。

objectFree是我们的释放内存的监听,它的函数定义是

typedef void (JNICALL *jvmtiEventObjectFree)
    (jvmtiEnv *jvmti_env,
     jlong tag);

可以看到,我们在释放内存的时候得到的信息非常有限,只有一个tag,也就是我们在分配内存时通过SetTag操作所得到的参数,如果有设置就就会为具体的tag数值。我们在这个函数中的业务逻辑就是记录当次的释放记录即可

void JNICALL objectFree(jvmtiEnv *jvmti_env,
                        jlong tag) {
    std::list<int>::iterator it = std::find(list1.begin(), list1.end(), tag);
    if (it != list.end()) // 找到了
    {
        __android_log_print(ANDROID_LOG_ERROR, "hello", "release %lld",tag);
        char str[500];
        char *format = "release tag %lld\r\n";
        //ALOGI(format, GetCurrentSystemTime().c_str(),threadInfo.name, classSignature, size, tag);
        sprintf(str, format,tag);
        memoryFile->write(str, sizeof(char) * strlen(str));
    }
}

我们再回到上述代码留下的疑问,list是个什么?其实就是记录了我们在VMObjectAlloc阶段所分配的属于我们自定义的类的tag,因为ObjectFree提供给我们的信息非常有限,只有一个tag,如果不通过这个list保存分配内存时的tag的话,就会导致释放的时候我们引入过多的不必要的释放记录。但是这里也带来了一个问题,就是我们需要时刻同步list的状态,因为jvmti是可以在多线程环境下回调,如果只是简单操作list的话就会带来同步问题(这里我们没有处理,为了demo的简单)真实操作上我们最好加入mutex锁或者其他机制保证同步问题。

下面我们再给出memoryFile->write的代码

currentSize 记录当前大小 m_size 以页为单位的默认大小
void MemoryFile::write(char *data, int dataLen) {
    mtx.lock();
    if(currentSize + dataLen >= m_size){
        resize(currentSize+dataLen);
    }
    memcpy(ptr + currentSize, data, dataLen);
    currentSize += dataLen;
    mtx.unlock();
}
void MemoryFile::resize(int32_t needSize) {
    // 如果mmap的大小不够,就需要重新进行mmap操作,以页为单位
    int32_t oldSize = m_size;
    do{
        m_size *=2;
    } while (m_size<needSize);
    ftruncate(m_fd, m_size);
    munmap(ptr, oldSize);
    ptr = static_cast<int8_t *>(mmap(0,m_size,PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0));
}

开启监听

到这里,我们还没有结束,我们需要真正的开启监听,前面只是设置监听的操作,我们可以通过SetEventNotificationMode函数开启真正监听/关闭监听

jvmtiError SetEventNotificationMode(jvmtiEventMode mode,
          jvmtiEvent event_type,
          jthread event_thread,
           ...) {
  return functions->SetEventNotificationMode(this, mode, event_type, event_thread);
}

mode代表当前状态,是个枚举,event_type就是我们要开启监听的类型(这里我们指定为内存分配与释放事件即可),event_thread可以指定某个线程的内存分配事件,null就是全局监听,所以我们的业务代码如下

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

java层开启agent

通过在native层设置了jvmti的监听与实现,我们还要在java层通过Debug.attachJvmtiAgent(9.0)进行开启,这里有细微差距

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.util.*
object MemoryMonitor {
    private const val JVMTI_LIB_NAME = "libjvmti-monitor.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, JVMTI_LIB_NAME)
            if (libSo.exists()) libSo.delete()
            val findLibrary =
                ClassLoader::class.java.getDeclaredMethod("findLibrary", String::class.java)
            val libFilePath = findLibrary.invoke(context.classLoader, "jvmti-monitor") as String
            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);
            val logDir = File(context.filesDir, "log")
            val path = "${logDir.absolutePath}/test.log"
            initMemoryCallBack(path)
        } else {
            Log.e("memory", "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)
        }
    }
    // 设置mmap的文件path
    external fun initMemoryCallBack(path: String)
}

attachJvmtiAgent方法需要实现了jvmti 的so库的绝对地址,那么我们如何查找一个so库的地址呢?其实就是通过ClassLoader的findLibrary方法,我们可以获取到so的绝对地址,不过这个绝对地址不能够直接用,我们看一下源码attachJvmtiAgent

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

其中attachJvmtiAgent 会进行格式校验Preconditions.checkArgument(!library.contains("=")),恰好我们得到的so的地址是包含=的,所以才需要一个File的copy操作(拷贝到一个不包含=的目录下)

验证分配数据

通过上面的jvmti操作,我们已经可以将数据保存到本地文件了,本地文件的保存可以自己定义,这里我保存在context.filesDir目录中/log子目录下,同时我们生成一个测试数据

package com.test.memory
data class TestData(val test:Int) {
}
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    binding.sampleText.text = "Hello World"
    TestData(1)
}

运行后

我们就完成了一个内存的记录,通过该记录我们就能够分析哪些类引起了内存问题(即存在分配tag不存在释放tag)

总结

到这里,我们终于完成了一个jvmti的监控操作!当然,上面的代码还有很多需要提升的地方,比如多线程引用,比如我们可以同时开启MethodEntry的callback记录一个方法的开始和结束,为内存泄漏的定位做更加详细的分析等等!因为篇幅有限,这里就当作拓展留给读者们自行实现啦,以上就是Android性能优化之JVMTI与内存分配的详细内容,更多关于Android性能JVMTI内存分配的资料请关注编程网其它相关文章!

--结束END--

本文标题: Android性能优化之JVMTI与内存分配

本文链接: https://lsjlt.com/news/169040.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Android性能优化之JVMTI与内存分配
    目录前言JVMTIJVMTI 简介:native层开启jvmti前置准备复写Agent开启jvmtiCapabilities设置jvmtiEventCallbacks开启监听java...
    99+
    2024-04-02
  • Android性能优化之内存优化的示例分析
    这篇文章主要介绍Android性能优化之内存优化的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!1、Android内存管理机制1.1 Java内存分配模型先上一张JVM将内存划分区域的图程序计数器:存储当前线...
    99+
    2023-06-15
  • 浅谈Android性能优化之内存优化
    目录1、Android内存管理机制1.1 Java内存分配模型1.2 Dalvik和ART介绍1.3 为什么要进行内存优化2、Java内存回收算法2.1判断Java中对象是否存活的算...
    99+
    2024-04-02
  • Android性能优化之内存泄漏
      前言   对于内存泄漏,我想大家在开发中肯定都遇到过,只不过内存泄漏对我们来说并不是可见的,因为它是在堆中活动,而要想检测程序中是否有内存泄漏的产生,通常我们可以借助...
    99+
    2022-06-06
    内存泄漏 优化 Android
  • Golang函数性能优化之存储分配优化
    为了提高 go 函数性能,存储分配优化至关重要。通过预分配缓冲区、使用切片和使用对象池等技术,可以有效减少内存分配带来的开销。以读取大文件为例,预分配文件行缓冲区可以显著优化性能,因为它...
    99+
    2024-04-16
    golang 性能优化
  • 详解Android性能优化之内存泄漏
    综述 内存泄漏(memory leak)是指由于疏忽或错误造成程序未能释放已经不再使用的内存。那么在Android中,当一个对象持有Activity的引用,如果该对象不能被系统...
    99+
    2022-06-06
    内存泄漏 优化 Android
  • Android性能优化之ViewPagers + Fragment缓存优化
    目录前言1 ViewPager懒加载优化1.1 ViewPager的缓存机制1.2 ViewPager懒加载方案2 ViewPager2与ViewPager的区别前言 大家看标题,可...
    99+
    2024-04-02
  • 优化内存分配与释放:提升操作系统性能
    内存管理在操作系统中的作用 内存管理是操作系统的一项核心功能,它负责管理计算机中的物理内存。物理内存是用于存储当前正在运行的程序和数据的有限资源。当程序请求内存时,操作系统会分配一个内存块并将其返回给该程序。当程序不再需要内存时,它会将...
    99+
    2024-02-17
    内存管理 内存分配 内存释放 操作系统性能 碎片化
  • Android内存优化之内存缓存
      什么是缓存?   缓存技术原理是把用户访问的所有对象看作一个全集,经过算法标记哪些是用户经常访问的对象,把这些对象放到一个集合里,这个集合是全集一个子集,下一次...
    99+
    2022-06-06
    优化 缓存 Android
  • Golang函数性能优化之内存对齐优化
    内存对齐优化通过将数据对齐到特定地址来提高程序性能。它减少缓存未命中、优化 simd 指令,具体步骤如下:使用 alignof 函数获取类型的最小对齐值。使用 unsafe.pointe...
    99+
    2024-04-17
    golang 内存对齐
  • android内存优化之图片优化
    对图片本身进行操作。尽量不要使用setImageBitmap、setImageResource、BitmapFactory.decodeResource来设置一张大图,因为这些...
    99+
    2022-06-06
    图片 优化 Android
  • Oracle 学习之 性能优化(十四) 内存
     Oracle数据库包含了如下基本内存组件System global area (SGA)The SGA is a group of shared memory structures, known...
    99+
    2024-04-02
  • PHP 性能优化:内存优化技巧
    内存管理对 php 性能优化至关重要。优化内存使用可以通过以下技巧实现:使用高效的数据结构,如数组而非链表。避免不必要的内存拷贝,使用引用 (&) 或赋值 (=) 进行变量传递。...
    99+
    2024-05-10
    php 内存优化 键值对
  • golang函数性能优化与内存管理
    在 go 语言中,优化函数性能和内存管理对于应用程序效率至关重要。通过优化这些方面,可以显著提高应用程序的响应速度和可靠性。具体措施包括:避免不必要的函数调用使用内联函数减少函数参数使用...
    99+
    2024-04-26
    golang 内存管理 性能优化 内存占用
  • Android性能优化之利用Rxlifecycle解决RxJava内存泄漏详解
    前言: 其实RxJava引起的内存泄漏是我无意中发现了,本来是想了解Retrofit与RxJava相结合中是如何通过适配器模式解决的,结果却发现了RxJava是会引起内存泄漏的...
    99+
    2022-06-06
    rxjava 优化 Android
  • Android 分析实现性能优化之启动速度优化
    目录启动方式冷启动(启动优化目标)热启动温启动启动流程中可优化的环节检测工具启动时间检测Logcat Displayedadb 命令统计CPU profileAPI level &g...
    99+
    2024-04-02
  • PHP底层的内存管理与性能优化
    PHP是一种流行的服务器端脚本语言,被广泛应用于Web开发。在进行PHP开发过程中,内存管理和性能优化是一个重要的话题。本文将讨论PHP底层的内存管理原理以及一些性能优化的技巧,并给出具体的代码示例。PHP内存管理原理PHP是一种解释型语言...
    99+
    2023-11-08
    PHP底层 内存管理 性能优化
  • 总结Android App内存优化之图片优化
    前言 在Android设备内存动不动就上G的情况下,的确没有必要去太在意APP对Android系统内存的消耗,但在实际工作中我做的是教育类的小学APP,APP中的按钮、背景、动...
    99+
    2022-06-06
    图片 优化 app Android
  • java性能优化之代码缓存优化
    目录JIT编译器版本默认情况JVM如何选择编译器?如何判断当前环境jvm使用的编译器?代码缓存代码缓存占满发生在什么情况?代码缓存默认大小如何确定正好的代码缓存?如何监控代码缓存?J...
    99+
    2024-04-02
  • 详解Android性能优化之启动优化
    目录1、为什么要进行启动优化2、启动的分类2.1 冷启动2.2 热启动2.3 温启动3、优化方向4、启动时间的测量方式4.1 使用adb 命令方式(线下使用方便)4.2 手动打点方式...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作