核桃干货 | Android常见内存泄漏与优化
Android虚拟机:Dalvik和ART
1.1 JVM与Dalvik区别
Java字节码以单字节(1 byte)为单元,JVM使用的指令只占1个单元;Dalvik字节码以双字节(2 byte)为单元,Dalvik虚拟机使用的指令占1个单元或2个单元。因此,在上面的代码中JVM字节码占11个单元=11字节,Dalvik字节码占6个单元=12字节(其中,mul-int/lit8指令占2单元)。
(2) 执行的字节码文件不同
JVM运行的.class文件,Dalvik运行的是.dex(即Dalvik Executable)文件。在Java程序中,Java类会编译成一个或多个.class文件,然后打包到.jar文件中,.jar文件中的每个.class文件里面包含了该类的常量池、类信息、属性等。
当JVM加载该.jar文件时,会加载里面的所有的.class文件,JVM的这种加载方式很慢,对于内存有限的移动设备并不合适;.dex文件是在.class文件的基础上,经过DEX工具压缩和优化后形成的,通常每一个.apk文件中只包含了一个.dex,这个.dex文件将所有的.class里面所包含的信息全部整合在一起了,这样做的好处就是减少了整体的文件尺寸(去除了.class文件中相同的冗余信息),同时减少了I/O操作,加快了类的查找速度。下图展示了.jar和.dex的对比差异:
(3) 在内存中的表现形式差异
Dalvik经过优化,允许在有限的内存中同时运行多个进程,或说同时运行多个Dalvik虚拟机的实例。
在Android中每一个应用都运行在一个Dalvik虚拟机实例中,每一个Dalvik虚拟机实例都运行在一个独立的进程空间中,因此都对应着一个独立的进程,独立的进程可以防止在虚拟机崩溃时所有程序都被关闭。而对于JVM来说,在其宿主OS的内存中只运行着一个JVM的实例,这个JVM实例中可以运行多个Java应用程序(进程),但是一旦JVM异常崩溃,就会导致运行在其中的所有程序被关闭。
(4) Dalvik拥有Zygote进程与共享机制
在Android系统中有个一特殊的虚拟机进程--Zygote,它是虚拟机实例的孵化器。它在Android系统启动的时候就会产生,完成虚拟机的初始化、库的加载、预制类库和初始化操作。
如果系统需要一个新的虚拟机实例,他会迅速复制自身,以最快的速度提供给系统。对于一些只读的系统库,所有的虚拟机实例都和Zygote共享一块区域。Dalvik虚拟机拥有预加载-共享的机制,使得不同的应用之间在运行时可以共享相同的类,因此拥有更高的效率。而JVM则不存在这个共享机制,不同的程序被打包后都是彼此独立的,即便它们在包里使用了相同的类,运行时的都是单独加载和运行,无法进行共享。
1.2 Dalvik与ART区别
ART虚拟机被引入于Android 4.4,用来替换Dalvik虚拟机,以缓解Dalvik虚拟机的运行机制导致Android应用运行变慢的问题。在Android 4.4中,可以选择使用Dalvik还是ART,而从Android 5.0开始,Dalvik被完全删除,Android系统默认采用ART。Dalvik与ART的主要区别如下:
(1) ART运行机制优于Dalvik
对于运行在Dalvik虚拟机实例中的应用程序而言,在每一次重新运行的时候,都需要将字节码通过JIT(Just-In-Time)编译器编译成机器码,这会使用应用程序的运行效率降低,虽然Dalvik虚拟机已经被做过很多优化(.dex文件->.odex文件),但由于这种先翻译再执行的机制仍然无法有效解决Dalvik拖慢Android应用运行的事实。
而在ART中,系统在安装应用程序时会进行一次AOT(Ahead Of Time compilication,预编译),即将字节码预先编译成机器码并存储在本地,这样应用程序每次运行时就不需要执行编译了,运行效率会大大提高。
(2) 支持的CPU架构不同
Dalvik是为32位CPU设计的,而ART支持64位并兼容32位的CPU。
(3) 运行时堆划分不同
Dalvik虚拟机的运行时堆使用标记--清除(Mark--Sweep)算法进行GC,它由两个Space以及多个辅助数据结构组成,两个Space分别是Zygote Space(Zygote Heap)和Allocation Space(Active Heap)。
Zygote Space用来管理Zygote进程在启动过程中预加载和创建的各种对象,Zygote Space中不会触发GC,应用进程和Zygote进程之间会共享Zygote Space。
Zygote进程在fork第一个子进程之前,会把Zygote Space分为两个部分,原来被Zygote进程使用的部分仍然叫Zygote Space,而剩余未被使用的部分被称为Allocation Space,以后fork的子进程相关的所有的对象都会在Allocation Space上进行分配和释放。
需要注意的是,Allocation Space不是进程共享的,在每个进程中都独立拥有一份。下图展示了Dalvik虚拟机的运行时堆结构:
安装时间变长。应用在安装的时候需要预编译,从而增大了安装时间。 存储空间变大。ART引入AOT技术后,需要更多的空间存储预编译后的机器码。
1.3 Dalvik/ART的启动流程
//app_main.cpp$main函数
int main(int argc, char* const argv[])
{
...
// (1) 创建AppRuntime对象
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
...
// (2) 解析执行init.rc的启动服务的命令传入的参数
// 解析后:zygote = true
// startSystemServer = true
// niceName = zygote (当前进程名称)
bool zygote = false;
bool startSystemServer = false;
bool application = false;
String8 niceName;
String8 className;
while (i < argc) {
const char* arg = argv[i++];
if (strcmp(arg, "--zygote") == 0) {
zygote = true;
niceName = ZYGOTE_NICE_NAME;
} else if (strcmp(arg, "--start-system-server") == 0) {
startSystemServer = true;
} else if (strcmp(arg, "--application") == 0) {
application = true;
} else if (strncmp(arg, "--nice-name=", 12) == 0) {
niceName.setTo(arg + 12);
} else if (strncmp(arg, "--", 2) != 0) {
className.setTo(arg);
break;
} else {
--i;
break;
}
}
...
// (3) 设置进程名为Zygote,执行ZygoteInit类
// Zygote = true
if (!niceName.isEmpty()) {
runtime.setArgv0(niceName.string());
set_process_name(niceName.string());
}
if (zygote) {
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
return 10;
}
}
创建AppRuntime实例。AppRuntime是在app_process.cpp中定义的类,继承于系统的AndroidRuntime,主要用于创建和初始化虚拟机。AppRuntime类继承关系如下:
class AppRuntime : public AndroidRuntime
{};
解析执行init.rc的启动服务的命令传入的参数。/init.zygote64_32.rc文件中启动Zygote的内容如下,在<Android源代码目录>/system/core/rootdir/ 目录下可以看到init.zygote32.rc、init.zygote32_64.rc、init.zygote64.rc、init.zygote64_32.rc等文件,这是因为Android5.0开始支持64位的编译,所以Zygote进程本身也有32位和64位版本。启动Zygote进程命令如下: /tasks service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
class main
priority -20
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart audioserver
onrestart restart cameraserver
onrestart restart media
onrestart restart netd
writepid /dev/cpuset/foreground/tasks /dev/stune/foreground
执行ZygoteInit类。由前面 解析命令传入的参数可知,zygote=true说明当前程序运行的进程是Zygote进程,将调用AppRuntime的start函数执行ZygoteInit类,从类名可以看出执行该类将进入Zygote的初始化流程。
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
该函数主要完成三个方面的工作:(a) 初始化JNI环境,启动虚拟机;(b) 为虚拟机注册JNI方法;(c)从传入的com.android.internal.os.ZygoteInit 类中找到main函数,即调用ZygoteInit.java类中的main方法。AndroidRuntime$start源码如下:
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
...
// (1) 初始化JNI环境、启动虚拟机
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);
// (2) 为虚拟机注册JNI方法
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
...
// (3) 从传入的com.android.internal.os.ZygoteInit 类中找到main函数,即调用
// ZygoteInit.java类中的main方法。AndroidRuntime及之前的方法都是native的方法,而此刻
// 调用的ZygoteInit.main方法是java的方法,到这里我们就进入了java的世界
char* slashClassName = toSlashClassName(className);
jclass startClass = env->FindClass(slashClassName);
if (startClass == NULL) {
ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
/* keep going */
} else {
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
ALOGE("JavaVM unable to find main() in '%s'\n", className);
/* keep going */
} else {
env->CallStaticVoidMethod(startClass, startMeth, strArray);
if (env->ExceptionCheck())
threadExitUncaughtException(env);
}
}
...
}
# __ANDROID__
#
#
// JniInvocation::Init
bool JniInvocation::Init(const char* library) {
// Android平台标志
char buffer[PROP_VALUE_MAX];
char* buffer = NULL;
// 获取“libart.so”或“libdvm.so”
library = GetLibrary(library, buffer);
const int kDlopenFlags = RTLD_NOW | RTLD_NODELETE;
// 加载“libart.so”或“libdvm.so”
handle_ = dlopen(library, kDlopenFlags);
if (handle_ == NULL) {
if (strcmp(library, kLibraryFallback) == 0) {
return false;
}
library = kLibraryFallback;
handle_ = dlopen(library, kDlopenFlags);
if (handle_ == NULL) {
ALOGE("Failed to dlopen %s: %s", library, dlerror());
return false;
}
}
...
return true;
}
从JniInvocation::Init函数源码可知,它首先会调用JniInvocation::GetLibrary函数来获取要指定的虚拟机库名称–“libart.so”或“libdvm.so”,然后调用JniInvocation::dlopen函数加载这个虚拟机库。
通过查阅JniInvocation::GetLibrary函数源码可知,如果当前不是Debug模式构建的,是不允许动态更改虚拟机动态库,即默认为"libart.so";如果当前是Debug模式构建且传入的buffer不为NULL时,就需要通过读取"persist.sys.dalvik.vm.lib.2"这个系统属性来设置返回的library。JniInvocation::GetLibrary函数源码如下:
static const char* kLibraryFallback = "libart.so";
const char* JniInvocation::GetLibrary(const char* library, char* buffer) {
return GetLibrary(library, buffer, &IsDebuggable, &GetLibrarySystemProperty);
}
const char* JniInvocation::GetLibrary(const char* library,
char* buffer,
bool (*is_debuggable)(),
int (*get_library_system_property)(char* buffer)) {
# __ANDROID__
const char* default_library;
// 如果不是debug构建,不允许更改虚拟机动态库
// library = default_library = kLibraryFallback = "libart.so"
if (!is_debuggable()) {
library = kLibraryFallback;
default_library = kLibraryFallback;
} else {
// 如果是debug构建,需要判断传入的buffer参数是否为空
// 如果不为空,default_library赋值为buffer
if (buffer != NULL) {
if (get_library_system_property(buffer) > 0) {
default_library = buffer;
} else {
default_library = kLibraryFallback;
}
} else {
default_library = kLibraryFallback;
}
}
#
UNUSED(buffer);
UNUSED(is_debuggable);
UNUSED(get_library_system_property);
const char* default_library = kLibraryFallback;
#
if (library == NULL) {
library = default_library;
}
return library;
}
// "persist.sys.dalvik.vm.lib.2"是系统属性
// 它的取值可以为libdvm.so或libart.so
int GetLibrarySystemProperty(char* buffer) {
# __ANDROID__
return __system_property_get("persist.sys.dalvik.vm.lib.2", buffer);
#
UNUSED(buffer);
return 0;
#
}
常见内存分析工具
2.1 Android Profiler
标注(1~6)说明: 1:用于强制执行垃圾回收事件的按钮;
2:用于捕获堆转储的按钮,即Dump the Java heap;
3:用于放大、缩小、复位时间轴的按钮;
4 :用于实时播放内存分配情况的按钮;
5:发生一些事件的记录(如Activity的跳转,事件的输入,屏幕的旋转);
6:内存使用量事件轴,它包括以下内容:一个堆叠图表。显示每个内存类别当前使用多少内存,如左侧的y轴和顶部的彩色健所示。
Java:从Java或Kotlin代码分配的对象的内存(重点关注); Native:从C或C++代码分配的对象的内存(重点关注); Graphics:图像缓存等,包括GL surfaces, GL textures等; Stack:栈内存(包括java和c/c++); Code:用于处理代码和资源(如 dex 字节码.so 库和字体)分配的内存; Other:系统都不知道是什么类型的内存,放在这里; Allocated:从Java或Kotlin代码分配的对象数。 一个堆叠图表。显示每个内存类别当前使用多少内存,如左侧的y轴和顶部的彩色健所示。 一条虚线。虚线表示分配的对象数量,如右侧的y轴所示(5000/15000)。
每个垃圾回收时间的图标。
2.1.1 Allocation Tracker
分配了哪些类型的对象,分配了多大的空间; 对象分配的栈调用,是在哪个线程中调用的; 对象的释放时间(只针对8.0+);
Allocations:堆中动态分配对象个数; Deallocations:解除分配的对象个数; Total Counts:目前存在的对象总数; Shallow Size:堆中所有对象的总大小(以字节为单位),不包含其引用的对象;
2.1.2 Heap Dump
该时刻应用分配了哪些类型的对象,每种对象有多少; 每个对象当前时刻使用了多少内存; 对象所分配到的调用堆栈(Android 7.1以下会有所区别);
下面我们解释下上图颜色方框中相关标签名表示的意义。
Allocations: 堆中分配对象的个数; Native Size:此对象类型使用的native内存总量。此列仅适用于Android 7.0及更高版本。您将在这里看到一些用Java分配内存的对象,因为Android使用native内存来处理某些框架类,例如Bitmap。 Shallow Size: 此对象类型使用的Java内存总量; Retained Size: 因此类的所有实例而保留的内存总大小;
Depth:从任意 GC root 到所选实例的最短 hop 数。 Native Size: native内存中此实例的大小。此列仅适用于Android 7.0及更高版本。 Shallow Size:此实例Java内存的大小。 Retained Size:此实例支配[dominator]的内存大小(根据 [支配树]
2.2 MAT
MAT,全称"Memory Analysis Tool",是对内存进行详细分析的工具,它是eclipse的一个插件,对于AS开发来说,需要单独下载MAT(当前最新版本为1.9.1)。
在上述图中,我们主要关注两个部分:饼状图和Actions,其中,饼状图主要用来显示内存的消耗,它的彩色部分表示被分配的内存,灰色部分则是空闲区域,单击每个彩色区域可以看到这块区域的详细信息;Actons一栏列出了4种Action,其作用与区别如下。
Historgram:列出每个类的所有对象。从类的角度进行分析,注重量的分析; Dominator Tree:列出大对象和它们的引用关系。从对象的角度分析,注重引用关系分析; Top Consumers:获取开销最大的对象,可通过类或包形式分组; Duplicate Classes:检测出被多个类加载器加载的类;
从上图可以看到,在Dominator Tree列出了很多SingleInstanceActivity的实例,而一般SingleInstanceActivity是不该有这么多实例的,因此,基本可以断定发生了内存泄漏,至于内存泄漏的具体原因,就需要查看GC引用链。但在查看之前,我们需要理解下红色方框几个标签的意义。
Shallow Heap
Retained Heap
对象C直接支配对象D、E、H,故C是D、E、H的父节点;
对象D直接支配对象F,故D是F的父节点;
对象E直接支配对象G,故E是G的父节点;