介绍

内存走漏是现在开发过程中十分头疼的问题,关于新功能代码,假如呈现内存走漏那能够经过重构代码来解决,可是关于老旧代码的内存走漏处理起来却是十分棘手的。本文是作者关于怎么运用JvmTi来处理老旧代码中呈现的内存走漏的一些考虑和测验完成介绍。

JvmTi简介

JvmTi(JVM Tool Interface):Java 虚拟机所供给的 native 编程接口。是Java虚拟机供给的一整套后门。经过这套后门能够对虚拟机方方面面进行监控,剖析。甚至干预虚拟机的运转。

JvmTi 本质上是在JVM内部的许多事情进行了埋点。经过这些埋点能够给外部供给当时上下文的一些信息。甚至能够承受外部的指令来改动下一步的动作。外部程序一般运用C/C++完成一个JvmTiAgent,在Agent里边注册一些JVM事情的回调。当事情产生时JVMTI调用这些回调办法。Agent能够在回调办法里边完成自己的逻辑。JvmTiAgent是以动态链接库的形式被虚拟机加载的。

JvmTi 接口供给的监控及控制能力

供给的控制函数
功能 函数名 介绍
分配内存 Allocate 函数Allocate经过JVMTI的内存分配器分配一块内存区域,经过该函数分配的内存,需求经过函数Deallocate开释掉。
开释分配的内存 Deallocate 该函数经过JVMTI的内存分配器开释由参数mem指向的内存区域,特别的,应该专用于由JVMTI函数分配的内存区域。分配的内存都应该被开释掉,避免内存走漏。
线程相关 函数名 介绍
获取线程状况 GetThreadState 回来(JVMTI_THREAD_STATE_ALIVEJVMTI_THREAD_STATE_RUNNABLE)等状况
获取当时线程 GetCurrentThread 该函数用于获取当时线程目标,这儿获取的在Java代码中调用该函数时所在的线程。
获取一切的线程 GetAllThreads 用于获取一切存活的线程,留意,这儿所说的是Java的线程,即一切连接到JVM的线程
暂停线程 SuspendThread 暂定方针线程。假如指定了方针线程,则会阻塞当时函数,直到其他线程对方针线程调用了函数ResumeThread。假如要暂停的是当时线程,则该函数啥也不干,回来过错
stack frame 函数名 介绍
获取某个线程当时的调用栈 GetStackTrace 该函数用于获取方针线程的栈信息。调用该函数时,无需挂起方针线程。
获取一切线程的调用栈 GetAllStackTraces 该函数用于获取一切存活线程的栈信息(包含JVMTI署理线程)。
弹出栈帧 PopFrame 该函数用于弹出线程栈帧。弹出顶层栈帧后,会将程序回到前一个栈帧。当线程恢复运转后,线程的履行状况会被置为调用当时办法之前的状况。
类特点相关函数
功能 函数名 介绍
设置/获取方针目标的标签 SetTag/GetTag 标签的值是一个长整型,一般用于存储一个仅有的ID值或是指向目标信息的指针。
获取指定目标的哈希值 GetObjectHashCode 哈希值可用于保护目标引证的哈希表,可是在某些JVM完成中,这可能会导致较大功能损耗,在大多数场景下,运用目标标签值来关联相应的数据是一个更有功率的办法。该函数确保了,目标的哈希值会在目标的整个生命周期内有效。
遍历堆中一切指定类的实例目标 IterateOverInstancesOfClass 直接继承和间接继承的,包含可达和不可达的。
获取局部变量 GetLocalObject/SetLocalObject 该函数用于获取/设置Object类型或其子类型的局部变量。

Android中运用

Android的Dalvik虚拟机或者是Art虚拟机都是根据Java虚拟机的,可是不幸的是Android体系一向并未供给相似的”后门”,直到8.0体系开端才完成了 JvmTi 1.2,不过现在看来Android8.0以上手机应该占绝大部分商场。

在Android中 JvmTi 又被称为 ArtTi ,它增加了一些限制如(摘自官网阐明):

首要,供给署理接口 JVMTI 的代码作为运转时插件(而不是运转时的核心组件)来完成。插件加载可能会受到限制,这样可阻止署理找到任何接口点, 其次,ActivityManager类和运转时进程只允许署理连接到可调试的运用。可调试运用由其开发者签核,以供剖析和插桩,而不会分发给最终用户。Google Play 商铺不允许发布可调试运用。这可确保普通运用(包含核心组件)无法遭到检测或操纵。

Android 中 JVMTI 和Agent的架构如图

基于JvmTI的动态内存释放

在Android中 运用JVMTI有两种办法

  1. 虚拟机启动时连接署理
  2. 运转时。将署理加载到当时进程中

在根据JVMTI 完成功能监控中提出了如在debug中运用这儿不做过多叙说。像上述作者所说的一样,因为jvmTi的强大,咱们能够做的事太多,例如:

  • 根据获取一切线程仓库的能力,运用采样的办法 生成函数调用火焰图
  • 根据 目标内存分配、开释函数完成 内存监控
  • 根据 MonitorContended 完成 锁等候监控
  • 根据 GarbageCollection 完成GC时长的监控

下面就介绍一下关于怎么凭借jvmTi来动态开释走漏目标。

完成原理

在之前的文章中有介绍过现在干流的两个内存走漏监控框架,运用监控开源库,咱们很简略得到内存走漏目标的引证链,不过能做的只有:修正已有代码,防止内存走漏。可是从上文中得知jvmti能够获取虚拟机中已有目标,并能够修正目标特点,那么咱们是就能够动态的修正走漏引证链来开释无法开释的目标。

解析走漏引证链

不管是LeakCannary仍是Koom都是能够经过解析hprof文件得到走漏引证链。如下图:

基于JvmTI的动态内存释放

从引证链中能够得到被走漏目标ClassName,在虚拟机中,同一时刻,同一ClassName的目标可能有多个,要处理走漏目标,必须要精确的选中被走漏目标。才能确保其他目标不会被误伤。

获取虚拟机中走漏目标

在Java中,判断两个目标是否持平,咱们能够经过hashcode值来比较,可是因为object中HashCode()办法可能存在被重写的可能,这样形成两个不同的目标具有持平hashcode值。所以hashcode()办法获取hashcode值来比较目标是不可取的。

可是在java虚拟机中,两个不同目标具有的hashCode值是必定的不持平,因而能够经过获取虚拟机中目标的hashcode来确保目标的仅有性。

JvmTi供给了IterateOverInstancesOfClass()能够获取到某个ClassName当时在虚拟机中存在的一切实例目标。这样咱们就能够经过遍历获取到的实例目标,并比照hashcode值(虚拟机中的hashCode值)来获取那个现已走漏的目标。


extern "C" JNIEXPORT jobjectArray findInstancesByClassImpl(JNIEnv *env,
                                                           jclass clazz, jclass clazz1) {
    if (!jvmtiInit || env == nullptr)
        return nullptr;
    jclass loadedObject = env->FindClass("java/lang/Object");
    localJvmtiEnv->IterateOverInstancesOfClass(clazz1, JVMTI_HEAP_OBJECT_EITHER, iterateMarkTag, 0);
    jint countObjs = 0;
    jobject *objects;
    jlong *tagResults;
    jlong idToQuery = 1;
    localJvmtiEnv->GetObjectsWithTags(iterateTag, &idToQuery, &countObjs, &objects, &tagResults);
    jobjectArray arrayReturn = env->NewObjectArray(countObjs, loadedObject, 0);
    for (int i = 0; i < countObjs; ++i) {
        env->SetObjectArrayElement(arrayReturn, i, objects[i]);
    }
    localJvmtiEnv->IterateOverInstancesOfClass(clazz1, JVMTI_HEAP_OBJECT_TAGGED, iterateCleanTag, 0);
    deallocate(tagResults);
    deallocate(objects);
    return arrayReturn;
}

HashCode的记载

上文中提到经过比较hashcode值来判断两个目标是否持平,所以咱们能够运用LeakCanary来记载走漏目标的hashcode值。

LeakCannary判断目标走漏的原理:LeakCannary在Activity/Fragment的生命周期结束时(OnDestroy办法履行)会将当时目标用WeakReference包装,这样在虚拟机GC的时分,会在ReferenceQueue中经过poll办法找到当时目标,也就意味这当时目标没有呈现内存走漏。假如不存在当时目标,那就意味这当时目标可能存在走漏,在延迟10s后手动触发GC,仍是没有获得当时目标的开释后,就认为此目标走漏。

在LeakCannary中记载hashcode值也是虚拟机分配个目标的值,而不是object的Hashcode()办法获取的值。

SetTag替换HashCode

在JvmTi文档中提到,当咱们获取某个目标虚拟机的hashcode值时是一件比较耗时的操作,而且关于hashcode值也简略引起误解,因而,咱们能够将上文中记载hashcode值的时机,换成给目标SetTag,这样能够起到相同的效果。

extern "C" JNIEXPORT void JNICALL setTagImpl(JNIEnv *env,
                                             jclass clazz, jobject obj) {
    if (env == nullptr || localJvmtiEnv==nullptr)
        return;
    jint hashcode;
    // 此处给目标设置tag,运用hashcode值作为目标的tag,实则需求将java层面定义的tag值传递到native层
    localJvmtiEnv->GetObjectHashCode(obj, &hashcode);
    localJvmtiEnv->SetTag(obj, hashcode);
    jclass cls = env->FindClass("java/lang/Object");
    jmethodID mid_getName = env->GetMethodID(cls, "toString", "()Ljava/lang/String;");
    jstring name = static_cast<jstring>(env->CallObjectMethod(obj, mid_getName));
    const char *toString = env->GetStringUTFChars(name, JNI_FALSE);
    ALOGI("obj:{%s} set tag:{%d} success", toString, hashcode);
}

Tag的值需求开发人员来确保仅有性。

修正持有走漏目标引证指向

现在,假定走漏目标为A,持有A引证的目标为B。经过上文的引证链和目标A的Tag值。就能够找到持有当时走漏目标A,那么也能够很简略找到目标B。那么开释走漏目标A最简略的办法便是将目标B指向目标A的引证设置为NULL:

extern "C" JNIEXPORT void
setObjectFieldValueImpl(JNIEnv *env, jclass thisClass, jobject obj, jobject field,
                        jclass fieldClass,
                        jboolean field_type, jobject newValue) {
    if (env == nullptr)
        return;
    jfieldID jfieldId = env->FromReflectedField(field);
    if (field_type)
        _setStaticFieldValue(env, env->GetObjectClass(obj), fieldClass, jfieldId, newValue);
    else
        _setFieldValue(env, obj, fieldClass, jfieldId, newValue); 
}

此处的newVlaue == null,可是有一些情况下并不能这么简略处理。比方老旧代码中已知的内存走漏,重构代码代价特别大,可是此目标又持有较大目标,如Bitmap\Handler等,为了节省内存空间,能够释走漏目标内部部分的持有。例如Activity的开释可分为以下几个等级:

  • NO_WINDOW : 没有window,当时Activity只持有Context。
  • NO_VIEW : 没有View, 当时Activity没有DecorView。
  • NO_CONTENT_VIEW : 没有ContentView, 当时Activity没有用户自定的View。
  • CUSTOM : 开释此Activity中的自定目标。

开释Activity中一切View的时分能够凭借Activity的DecorView持有的ViewRootImpl,在ViewRootImpl中有持有一个Handler,当这个Handler履行sendEmptyMessage(3)时,会履行ViewRootImpl的doDie()办法,这个办法能够安全的移除一切View。

总结

运用JvmTi能够做的事情必定不仅限于此,上述关于内存走漏的动态开释只是一个初步的测验,最多算是一个demo,其中有很多不成熟的想法后期慢慢能够完善,要想用于工程必定还有很长的路要走。希望有爱好的小伙伴能够多多测验和留言讨论!

参考文档

JvmTi Hepler

JvmTi oracle 官方文档

JvmTi开发文档

ART Ti

根据JVMTI 完成功能监控