面试中经常会问到内存优化,我们在开发过程中也多少会遇到OOM的问题,根据大牛们的博客,记录下我的学习思路
一、为何会OOM?
1. 一直以来Andorid手机的内存都比iPhone(iPhone6RAM1G)大的多,Android却经常出现OOM,这是为何?
这个是因为Android系统对dalvik的vm heapsize 作了硬性限制,当java进程申请的java空间超过阀值时,就会抛出OOM异常(这个阀值可以是48M、24M、16M等,视机型而定),可以通过adb shell getprop | grep dalvik.vm.heapgrowthlimit查看此值。(我的一加2 Android6.0.1已经达到了256M)
也就是说,程序发生OOM并不表示RAM不足,而是因为程序申请的java heap对象超过了dalvik.vm.heapgrowthlimit。也就是说,在RAM充足的情况下,也可能发生OOM
这样的设计似乎有些不合理,但是Google为什么这样做呢?这样设计的目的是为了让Android系统能同时让比较多的进程常驻内存,这样程序启动时就不用每次都重新加载到内存,能够给用户更快的响应。迫使每个应用程序使用较小的内存,移动设备非常有限的RAM就能使比较多的app常驻其中。
2. 大型游戏如何在较小的heapsize上运行?
- 创建子进程
创建一个新的进程,那么我们就可以把一些对象分配到新进程的heap上了,从而达到一个应用程序使用更多的内存的目的,当然,创建子进程会增加系统开销,而且并不是所有应用程序都适合这样做,视需求而定。
创建子进程的方法:使用android:process标签
- 使用jni在native heap上申请空间(推荐使用)
nativeheap的增长并不受dalvik vm heapsize的限制,从图6可以看出这一点,它的native heap size已经远远超过了dalvik heap size的限制。
只要RAM有剩余空间,程序员可以一直在native heap上申请空间,当然如果 RAM快耗尽,memory killer会杀进程释放RAM。大家使用一些软件时,有时候会闪退,就可能是软件在native层申请了比较多的内存导致的。比如,我(余龙飞)就碰到过UC web在浏览内容比较多的网页时闪退,原因就是其native heap增长到比较大的值,占用了大量的RAM,被memory killer杀掉了。(Fresco使用的就是这种方式)
- 使用显存(操作系统预留RAM的一部分作为显存)
使用OpenGL textures等API,texture memory不受dalvik vm heapsize限制,这个我没有实践过。再比如Android中GraphicBufferAllocator申请的内存就是显存。
3. Android内存究竟如何?(native heap、java heap)
- 进程的地址空间
在32位操作系统中(Native Process),进程的地址空间为0到4GB:
- kernel space(内河空间):这些地址用户代码不能读也不能写
- Memory Mapping Segment(内存映射):段文件映射(包括动态库)和匿名映射。
- Stack:(进栈和出栈)由操作系统控制,其中主要存储函数地址、函数参数、局部变量等等,所以Stack空间不需要很大,一般为几MB大小。
- Heap:空间的使用由程序员控制,程序员可以使用malloc、new、free、delete等函数调用来操作这片地址空间。Heap为程序完成各种复杂任务提供内存空间,所以空间比较大,一般为几百MB到几GB。正是因为Heap空间由程序员管理,所以容易出现使用不当导致严重问题。
- Android中的进程
- native进程:采用C/C++实现,不包含dalvik实例的linux进程,/system/bin/目录下面的程序文件运行后都是以native进程形式存在的。如下图/system/bin/surfaceflinger、/system/bin/rild、procrank等就是native进程。
- java进程:实例化了dalvik虚拟机实例的linux进程,进程的入口main函数为java函数。dalvik虚拟机实例的宿主进程是fork()系统调用创建的linux进程,所以每一个android上的java进程实际上就是一个linux进程,只是进程中多了一个dalvik虚拟机实例。因此,java进程的内存分配比native进程复杂。如图3,Android系统中的应用程序基本都是java进程,如桌面、电话、联系人、状态栏等等。
- Android中进程的堆内存
第一张图和下面这张图分别介绍了native process和java process的结构,这个是我们需要深刻理解的,进程空间中的heap空间是我们需要重点关注的。heap空间完全由程序员控制,我们使用的malloc、C++ new和java new所申请的空间都是heap空间, C/C++申请的内存空间在native heap中,而java申请的内存空间则在dalvik heap中。
注:Java中的code segment,data segment,heap,stack stack(栈):对象引用都是在栈里的,相当于C/C++的指针 heap(堆):new出来的对象实例才是在堆里 data segment:一般存放常量和静态常量 code segment:方法,函数什么的都是放在code segment
- Bitmap分配在native heap还是dalvik heap上?
3.0后是分配在dalvik heap上,和3.x之前是分配在native heap
4. 以上主要来自:现任支付宝大神余龙飞著作——Android进程的内存管理分析
二、内存分析之MAT
1. 谷歌提供了几种内存检测工具:
- Memory Monitor:内存监视器
- Heap Viewer:堆查看器
- Allocation Tracker:分配追踪器
- Investigating Your RAM Usage:调查您的RAM使用
通过Log输出的GC命令来判断: GC_CONCURRENT:heap快满了 GC_FOR_MALLOC:因为你的应用程序试图分配内存时,你已经充分引起GC堆,所以系统必须停止你的应用和回收内存。 GC_HPROF_DUMP_HEAP:GC发生时,你要创建的请求HPROF文件来分析你的堆。 GC_EXPLICIT:一个明确的GC,当收到调用gc()时出现,应该尽量避免手动调用,而是相信GC会自动清理 GC_EXTERNAL_ALLOC:只会在API10以及以下才会出现 GC的原因: Concurrent:不会暂停应用线程,在后台运行,不会影响内存分配 Alloc:GC是因为你的应用程序试图分配内存时,你heapwas已满。在这种情况下,垃圾收集发生在分配线程。 Explicit:手动调用gc(),我们应该避免手动调用,我们要相信GC,手动调用会影响线程分配以及没必要的cpu周期,还可能导致其他线程的抢占。 NativeAlloc:native内存的回收,主要来自人为造成的native内存压力,例如:Bitmap、渲染脚本分配的对象 CollectorTransition:.....由于用到的太少,后面的就不再详述
2. 触发内存泄漏
- 多次切换屏幕的横纵,在Activity不同状态旋转屏幕,然后再返回。旋转设备往往会导致应用程序的Activity、Context、或View对象泄漏,因为系统中重新创建活动,如果程序中其他地方拥有这些对象的引用,系统无法回收。
- 多个应用之间切换,在Activity不同的状态下切换(导航至主屏幕,然后返回到您的应用程序)。
3. 怎样的内存是健康的?
内存使用率低,使用率稳定(波动小)
- 没有正在使用的对象,要
能够被GC回收
(避免内存泄漏) - 不再使用的内存对象、或着大型内存,
使用结束(虚引用)马上回收
(finalize()方法进行清理
,通过 java.lang.ref.PhantomReference实现)我们进行内存分析具体分析什么呢? 1. 大型对象 2. 不使用的未能被释放的对象(内存泄漏) 而谷歌目前提供的内存分析工具只能从宏观上进行内存分析,无法针对某个对象进行分析 这里我们这里需要使用强大的第三方内存分析工具MAT(Memory Analyzer Tool)针对具体内存进行分析
4. MAT基础知识
- MAT简介
- MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。
- 当然MAT也有独立的不依赖Eclipse的版本,只不过这个版本在调试Android内存的时候,需要将DDMS生成的文件进行转换,才可以在独立版本的MAT上打开。不过Android SDK中已经提供了这个Tools,所以使用起来也是很方便的。
- MAT下载地址
- 独立版本下载地址: https://eclipse.org/mat/downloads.php
这种方式有个麻烦的地方就是DDMS导出的文件,需要进行转换才可以在MAT中打开。
- Eclipse插件地址:http://download.eclipse.org/mat/1.5/update-site/
- 独立版本下载地址: https://eclipse.org/mat/downloads.php
- MAT中重要概念介绍
要看懂MAT的列表信息,Shallow heap、Retained Heap、GC Root 这几个概念一定要弄懂。
- Shallow heap
Shallow size就是对象本身占用内存的大小,不包含其引用的对象。
- 常规对象
(非数组)的Shallow size有其成员变量的数量和类型决定。
数组的shallow size有数组元素的类型(对象类型、基本类型)和数组长度决定
因为不像c++的对象本身可以存放大量内存,java的对象成员都是些引用。真正的内存都在堆上,看起来是一堆原生的byte[],char[], int[],所以我们如果只看对象本身的内存,那么数量都很小。所以我们看到Histogram图是以Shallow size进行排序的,排在第一位第二位的是byte,char 。
- 常规对象
- Retained Heap
Retained Heap的概念:如果一个对象被释放掉,那么该对象引用的所有对象(包括被递归释放的)占用的heap也会被释放。
如果一个对象的某个成员new了一大块int数组,那这个int数组也可以计算到这个对象中。
与shallow heap比较,Retained heap可以更精确的反映一个对象实际占用的大小
(因为如果该对象释放,retained heap都可以被释放)。注意:A和B都引用到同一内存,A释放时,该内存不会被释放。所以这块内存不会被计算到A或者B的Retained Heap中。故
Retained Heap并不总是那么有效。
这一点并不重要,因为MAT引入了Dominator Tree--对象引用树,计算Retained Heap就会非常方便,显示也非常方便。对应到MAT UI上,在dominator tree这个view中,显示了每个对象的shallow heap和retained heap。然后可以以该节点为树根,一步步的细化看看retained heap到底是用在什么地方了。
- GC Root
GC发现通过任何reference chain(引用链)无法访问某个对象的时候,该对象即被回收。
名词GC Roots正是分析这一过程的起点,例如JVM自己确保了对象的可到达性(那么JVM就是GC Roots),所以GC Roots就是这样在内存中保持对象可到达性的,一旦不可到达,即被回收。
通常GC Roots是一个在current thread(当前线程)的call stack(调用栈)上的对象(例如方法参数和局部变量),或者是线程自身或者是system class loader(系统类加载器)加载的类以及native code(本地代码)保留的活动对象。所以
GC Roots是分析对象为何还存活于内存中的利器。
- Shallow heap
- MAT界面功能介绍
- 打开经过转换的hprof文件:
可不选
- Actions区域,几种分析方法:
- Histogram:列出内存中的对象,对象的个数以及大小
- Dominator Tree:列出最大的对象以及其依赖存活的Object (大小是以Retained Heap为标准排序的)
点开每个对象,
检查内部的超大对象
- Top Consumers : 通过图形列出最大的object
一般Histogram和 Dominator Tree是最常用的。
- 打开经过转换的hprof文件:
- MAT分析对象的引用
- Path to GC Root
在Histogram或者Domiantor Tree的某一个条目上,右键可以查看其GC Root Path:
- 点击Path To GC Roots –> with all references
通过这个图
查看(内存泄漏)
该内存还被谁所引用,为何还不能释放
- Path to GC Root
- MAT基础介绍来自Gracker
三、内存问题总览
- 内存泄漏
- 非静态内部类的静态实例容易造成内存泄漏
非静态内部类的存活需要依赖外部类
- Activity使用静态成员(静态成员引用Drawable、Bitmap等大内存对象)
- 使用handler时的内存问题
因为Handler的非即时性,导致部分代码不能及时释放
可以使用Badoo开发的第三方的 WeakHandler
- 注册某个对象后未反注册
注册广播接收器、注册观察者等等
- 集合中对象没清理造成的内存泄露
- 资源对象没关闭造成的内存泄露
资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。
- 非静态内部类的静态实例容易造成内存泄漏
- 一些不良代码成内存压力
- 循环以及递归
- 数据随意申请大小
- 如果没有用到不要定义全局变量
- Bitmap使用不当
- 及时的销毁
虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过Java堆的限制。因此,在用完Bitmap时,要
及时的recycle掉
。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。- 设置一定的采样率(二次采样)
有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。
- 巧妙的运用软引用(SoftRefrence)
有些时候,我们使用Bitmap后没有保留对它的引用,因此就无法调用Recycle函数。这时候巧妙的运用软引用,可以使Bitmap在内存快不足时得到有效的释放
目前但凡是个图片加载框架都会使用SoftRefrence
- 构造Adapter时,没有使用缓存的 convertView
- 频繁的方法中创建对象
不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。可以适当的使用 hashtable , vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次 new 之后又丢弃。
四、 MAT内存分析
- 使用Dominator Tree -> Path To GC Roots –> with all references
上面MAT基础里面已经讲
- 根据某种类型的对象个数来分析内存泄漏。
Actions -> Histogram
上图展示了内存中各种类型的对象个数和Shallow heap,我们看到byte[]占用Shallow heap最多,那是因为Honeycomb之后Bitmap Pixel Data的内存分配在Dalvik heap中。右键选中byte[]数组,选择List Objects -> with incomingreferences,可以看到byte[]具体的对象列表:
我们发现第二个byte[]的Retained heap较大,内存泄漏的可能性较大,因此右键选中这行,Path To GC Roots -> exclude weak references,同样可以看到上文所提到的情况,我们的Bitmap对象被leak所引用到,这里存在着内存泄漏。
- 讲的是对象及其应用的内存大小
- 讲的是大型对象被谁所引用
内存分析工具还有一个比较流行的内存泄漏检测库:LeakCannary