前言
这次分三篇文章分享Android内存管理与检测的办法,分别是内存管理概要与泄漏的原理,内存泄漏进程定位,内存泄漏进程内部代码段定位。本来还有几项如在lmkd基础上设计的增强型内存管理机制,Native进程泄漏定位方法等,由于涉及到专利部分所以就不公开了。要分享的三篇文章是我在实际项目中使用并不断改善总结的,相信大家看懂之后会有很大用处。
目录
3.3 AndroidStudio Memory Monitor
1. 背景介绍
1.1 JVM内存分配策略
Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。
静态存储区:主要存放静态数据、全局static数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
栈区 :当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时可由GC来负责回收。
1.2 Android虚拟机内存有关数据
Dalvik.vm.heapstartsize
应用堆创建时的起始大小
Dalvik.vm.heapgrowthlimit
应用申请内存的最大增长值
Dalvik.vm.heapsize
应用申请内存的最大值,在android开发中,如果要使用大堆,需要在 manifest中 指定android:largeHeap为true。这样dvm heap最大可达dalvik.vm.heapsize。
1.3 Memory Leak
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。
1.4 Out Of Memory
内存溢出,是指程序在申请堆内存时,已申请内存达到虚拟机上限,无法获得足够的剩余内存空间继续为应用创建对象,导致虚拟机抛出OutOfMemoryError异常。造成OOM异常的原因有可能是内存泄漏,也可能是对象创建过于庞大。
一种OOM异常后抛出的Log信息
2. DVM下的内存管理
DVM的内存管理包括对象的分配和释放工作。在Java中需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。内存的分配是由程序完成的,而内存的释放是由 GC 完成的。为了能够及时正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都会进行监控。
为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,通过有向图判断对象的可达性后,在Dalvik中GC系统通过Mark-Sweep算法将不可达的对象内存进行回收,实现内存释放。
通过下面的例子简单说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配关系情况。如红色箭头处所示:
3. 几种内存泄漏的检测方法
3.1 dumpsys meminfo
重点关注如下几个字段:
Ø Native/Dalvik 的 Heap 信息
具体在上面的第一行和第二行,它分别给出的是JNI层和Java层的内存分配情况,如果发现这个值一直增长,则代表程序可能出现了内存泄漏。
Ø Total 的 PSS 信息
这个值就是你的应用真正占据的内存大小,通过这个信息,你可以轻松判别系统中哪些程序占内存比较大了。
3.2 Prokrank
在Linux下表示内存的耗用情况有四种不同的表现形式:
VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)
PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
procrank -h
3.3 AndroidStudio Memory Monitor
常用功能说明:
Ø dump java heap
点击Dump Java Heap后,APP会Freeze住。大概几十秒后,就会进入读取hprof文件的界面了,如下图,在后面会继续讲解如何使用hprof文件。
Ø Starg Allocation Tracking
点击按钮后,开始分配追踪,过一些时间后,点击Stop Allocation Tracking结束追踪的位置。这样就截取了一段要分析的内存,等待几秒钟AndroidStudio会给我们打开一个Allocation视图
这个视图数据主要分析各个线程所占用内存的大小,对跟踪多线程应用有很大帮助。不过此工具似乎只能用于在线应用的内存跟踪,对于Android系统和离线应用目前没有找到使用办法。
3.4 DDMS MAT
打开DDMS界面,在左侧面板中选择要观察的进程,点击左上角的Update Heap按钮,再点击右侧面板中的Heap标签,然后点一下Cause GC按钮,此时表中的数值就会不断刷新显示当前的内存分配数值情况。
需要注意一个值:Heap 视图中部有一个 Type 叫做 data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在 data object 一行中有一列是“Total Size”,其值就是当前进程中所有 Java 数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:
不断的操作当前应用,同时注意观察 data object 的 Total Size 值。正常情况下 Total Size 值都会稳定在一个有限的范围内,也就是说由于程序中的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象 ,而在虚拟机不断的进行 GC 的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平。反之如果代码中存在没有释放对象引用的情况,则 data object 的 Total Size 值在每次 GC后不会有明显的回落,随着操作次数的增多 Total Size 的值会越来越大,直到到达一个上限后导致进程被 kill 掉。
MAT功能还有很多,要在实际当中结合实际情况摸索,实现不同方式下的内存跟踪。
3.5 LeakCanary
LeakCanary是一个开源的检测内存泄露的java库。它实际上就是在本机上自动做了Heap dump,对生成的hprof文件进行分析,展示结果。和手工分析Heap Dump的方式得到的结果是一样的。
4.常见内存泄漏场景及措施
4.1 内存泄漏——资源对象未关闭
使用Cursor、File、Socket等使用完成后需要在finally中关闭;
BroadCastReceiver、Service,绑定广播和服务,一定要记得在不需要的时候给解绑;
以addListener等方式注册回调接口后,如果不需要回调,应当remove掉回调接口;
4.2 内存泄漏——过度使用static成员变量
如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。
不要在类初始时初始化静态成员。可以考虑lazy初始化。
4.3 内存泄漏——单例
分析:
传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长 ;
传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。
措施:
4.4 内存泄漏——非静态内部类
范例一:
分析:
由于非静态内部类会隐式的持有外部类的引用,上面的代码由于内部类Demo实例对象被静态变量sInstance 引用,故造成Activity退出时也被sInstance 间接引用而不能被GC回收。
范例二:
分析:
由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,另外消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏,不过由Handler造成的内存泄漏一般是临时性的。
范例三:
分析:
上面的异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏。
措施:
- 定义静态内部类,去除外部类引用影响
- 使用软引用SoftReference或弱引用WeakReference,保证在必要时让GC回收
- 在onDestroy函数中消除全局变量的引用
4.5 内存泄漏——HashSet
分析:
由于HashSet key值是由对象hashcode得到,对对象元素进行修改会改变对象的hashcode,导致remove键值对失败。此时继续对集合进行操作将会导致集合大小不按预期的增大,产生内存泄漏现象。
措施:
不要把对象的可变域作为hashcode的计算依据,否则会出现各种意想不到的情况。比如contains失效、remove失效等等。如果把可变对象作为HashMap的key后并改变其域,则会出现明明有一个key-value对,但是却无法使用map的get方法从key获得value的现象。
4.6 内存泄漏——JNI
JNI开发过程中必然涉及到LocalReference的使用,JNI Local Reference 的生命期是在 native method 的执行期(从 Java 程序切换到 native code 环境时开始创建,或者在 native method 执行时调用 JNI function 创建),在 native method 执行完毕切换回 Java 程序时,所有 JNI Local Reference 被删除,生命期结束。
实际上,每当线程从 Java 环境切换到 native code 上下文时(J2N),JVM 会分配一块内存,创建一个 Local Reference 表,这个表用来存放本次 native method 执行中创建的所有的 Local Reference。每当在 native code 中引用到一个 Java 对象时,JVM 就会在这个表中创建一个 Local Reference。比如,实例 1 中我们调用 NewStringUTF() 在 Java Heap 中创建一个 String 对象后,在 Local Reference 表中就会相应新增一个 Local Reference。
由上面对Local Reference的生命周期可知,Local Reference表只有在JNI函数执行完成后才会被JVM回收,那么在JNI执行过程中依然会有OOM的情况。
多数情况下,在执行一个native方法时,你不需要担心局部引用的释放,java VM会在native方法返回调用者的时候释放。但在上面单个native方法调用中,创建了大量的局部引用。这可能会导致JNI局部引用表溢出。此时有必要及时地删除那些不再被使用的局部引用,以避免过高的内存使用。
5.NativeHeap
由于native heap的增长一般不受dalvik vm heapsize的限制,所以应用在需要大空间内存时,可以考虑使用native空间,而native空间必须在native中申请,所以java应用可以通过JNI来创建,范例代码如下:
说明:new或者malloc申请的内存是虚拟内存,申请之后不会立即映射到物理内存,即不会占用RAM,只有调用memset使用内存后,虚拟内存才会真正映射到RAM。
6.Linux共享内存
6.1 共享内存
共享内存是Linux进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。
在Android系统中,同样可以使用封装过后的匿名共享内存类,它能够辅助内存管理系统来有效地管理内存,在应用层的封装调用接口为MemoryFile(Java类),本地层调用接口为MemoryHeapBase、MemoryBase(C++类)。
6.2 共享内存在内存泄漏中的应用
由于匿名共享内存是通过内核驱动在内和空间创建,然后映射到应用空间使用的,这块空间与java虚拟机堆空间不在同一范围,不受虚拟机GC管控,所以匿名共享内存也能够在应用内存紧张时提供一个可靠的解决办法。
6.3 共享内存驱动数据
使用共享内存时,需要关注以下数据的配置,
SHMMAX
限制一个共享内存段的最大长度,单位是字节
SHMMNI
限制整个系统可以创建的最大的共享内存段的个数
SHMALL
限制系统用在共享内存上的内存页总数。一页一般是4k
RK3288下数据如下:
6.4 应用层使用示例