介绍
内存走漏是现在开发过程中十分头疼的问题,关于新功能代码,假如呈现内存走漏那能够经过重构代码来解决,可是关于老旧代码的内存走漏处理起来却是十分棘手的。本文是作者关于怎么运用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_ALIVE 、JVMTI_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的架构如图
在Android中 运用JVMTI有两种办法
- 虚拟机启动时连接署理
- 运转时。将署理加载到当时进程中
在根据JVMTI 完成功能监控中提出了如在debug中运用这儿不做过多叙说。像上述作者所说的一样,因为jvmTi的强大,咱们能够做的事太多,例如:
- 根据获取一切线程仓库的能力,运用采样的办法 生成函数调用火焰图
- 根据 目标内存分配、开释函数完成 内存监控
- 根据 MonitorContended 完成 锁等候监控
- 根据 GarbageCollection 完成GC时长的监控
下面就介绍一下关于怎么凭借jvmTi来动态开释走漏目标。
完成原理
在之前的文章中有介绍过现在干流的两个内存走漏监控框架,运用监控开源库,咱们很简略得到内存走漏目标的引证链,不过能做的只有:修正已有代码,防止内存走漏。可是从上文中得知jvmti能够获取虚拟机中已有目标,并能够修正目标特点,那么咱们是就能够动态的修正走漏引证链来开释无法开释的目标。
解析走漏引证链
不管是LeakCannary仍是Koom都是能够经过解析hprof文件得到走漏引证链。如下图:
从引证链中能够得到被走漏目标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 完成功能监控