发布时间:2023-09-15 11:30
做内存优化的目的是降低OOM率、减少卡顿、增加应用存活时间。
降低OOM率
做内存优化的一个常见原因是为了降低OOM率。
申请内存过多而没有及时释放,常常就会导致OOM。
引起OOM的原因有多种,在后面我们再细谈。
减少卡顿
Android中造成界面卡顿的原因有很多种,其中一种就是由内存问题引起的。
内存问题之所以会影响到界面流畅度,是因为垃圾回收。
在GC时,所有线程都要停止,包括主线程.当GC和绘制界面的操作同时触发时,绘制的执行就会被搁置,导致掉帧,也就是界面卡顿。
增加应用存活时间
Android会按照特定的机制清理进程,清理进程时优先会考虑清理后台进程,如果某个应用在后台运行并且占用的内存更多,就会被优先清理掉。
我们通常希望App能尽量存活的久一点,所以内存不再使用时应该尽快释放。
学习地址 :AndroidT10级高工必备性能优化合集
虚拟内存是程序和物理内存之间引入的中间层,目的是解决直接使用物理内存带来的安全性问题、超过物理内存大小需求无法满足等等问题。
而Linux的内存管理就是建立在虚拟内存之上的。虚拟地址与物理地址通过页表建立映射关系,CPU通过MMU(Memory Management Unit :内存管理单元)访问页表来查询虚拟地址对应的物理地址。虚拟地址分为内核空间和用户空间,它们对应的虚拟地址分别为进程共享和进程隔离的。
内核把page作为内存管理的基本单位。对特性不同的page又以zone来做划分,zone又由node来管理。
主要关注的区有3个:
区 | 描述 |
---|---|
ZONE_DMA | 直接内存访问,无需映射 |
ZONE_NORMAL | 一一对应映射页 |
ZONE_HIGHMEM | 动态映射页 |
每个zone中内存的组织形式是基于buddy伙伴算法,把空闲的page以2的n次方为单位进行管理。因此Linux最底层的内存申请都是以2的n次方为单位来申请page的。
Buddy伙伴算法以产生内部碎片为代价来避免外部碎片的产生。Linux针对大内存的物理地址分配,采用Buddy伙伴算法,如果是针对小于一个page的内存,频繁的分配和释放,则不宜用Buddy伙伴算法,取而代之的是Slab。
Slab是为频繁分配/释放的对象建立高速缓存。
用户空间主要分两部分,一个是面向C++的native层,一个是基于虚拟机的java层。
native内存划分:
java基于虚拟机的内存划分:
在Android系统中,堆实际上就是一块匿名共享内存。Android虚拟机仅仅只是把它封装成一个 mSpace,由底层C库来管理,并且仍然使用libc提供的函数malloc和free来分配和释放内存。
大多数静态数据会被映射到一个共享的进程中。常见的静态数据包括Dalvik Code、app resources、so文件等等。
在大多数情况下,Android通过显示分配共享内存区域(如Ashmem或者Gralloc)来实现动态RAM区域能够在不同进程之间共享的机制。例如,Window Surface在App和Screen Compositor之间使用共享的内存,Cursor Buffers在Content Provider和Clients之间共享内存。
上面说过,对于Android Runtime有两种虚拟机,Dalvik 和 ART,它们分配的内存区域块是不同的,下面我们就来简单了解下。
不管是Dlavik还是ART,运行时堆都分为 LinearAlloc(类似于ART的Non Moving Space)、Zygote Space 和 Alloc Space。
Dalvik中的Linear Alloc是一个线性内存空间,是一个只读区域,主要用来存储虚拟机中的类,因为类加载后只需要只读的属性,并且不会改变它。把这些只读属性以及在整个进程的生命周期都不能结束的永久数据放到线性分配器中管理,能很好地减少堆混乱和GC扫描,提升内存管理的性能。
Zygote Space在Zygote进程和应用程序进程之间共享,Allocation Space则是每个进程独占。Android系统的第一个虚拟机由Zygote进程创建并且只有一个Zygote Space。但是当Zygote进程在fork第一个应用程序进程之前,会将已经使用的那部分堆内存划分为一部分,还没有使用的堆内存划分为另一部分,也就是Allocation Space。但无论是应用程序进程,还是Zygote进程,当他们需要分配对象时,都是在各自的Allocation Space堆上进行。
当在ART运行时,还有另外两个区块,即 ImageSpace和Large Object Space。
注意:Image Space的对象只创建一次,而Zygote Space的对象需要在系统每次启动时,根据运行情况都重新创建一遍。
在Android的高级系统版本中,针对Heap空间有一个Generational Heap Memory的模型,其中将整个内存分为三个区域:
模型示意图如下所示:
由一个Eden区和两个Survivor区组成,程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当此Survivor区满时,此区存活的对象又被复制到另一个Survivor区,当这个Survivor区也满时,会将其中存活的对象复制到年老代。
一般情况下,年老代中的对象生命周期都比较长。
用于存放静态的类和方法,持久代对垃圾回收没有显著影响。
系统在Young Generation、Old Generation上采用不同的回收机制。每一个Generation的内存区域都有固定的大小。随着新的对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阈值时,会触发GC操作,以便腾出空间来存放其他新的对象。
此外,执行GC占用的时间与Generation和Generation中的对象数量有关,如下所示:
由于其对象存活时间短,因此基于Copying算法(扫描出存活的对象,并复制到一块新的完全未使用的控件中)来回收。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在Young Generation区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。
由于其对象存活时间较长,比较稳定,因此采用Mark(标记)算法(扫描出存活的对象,然后再回收未被标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减少内存碎片带来的效率损耗)来回收。
与C++不用,在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但也随之带来了内存泄漏的可能。简单点说:对于 C++ 来说,内存泄漏就是new出来的对象没有 delete,俗称野指针;而对于 java 来说,就是 new 出来的 Object 放在 Heap 上无法被GC回收。
Android使用的主要开发语言是Java所以二者的GC机制原理也大同小异,所以我们只对于常见的JVM GC机制的分析,就能达到我们的目的。我还是先看看那二者的不同之处吧。
Dalvik虚拟机(DVM)是Android系统在java虚拟机(JVM)基础上优化得到的,DVM是基于寄存器的,而JVM是基于栈的,由于寄存器高效快速的特性,DVM的性能相比JVM更好。
Dalvik执行.dex
格式的字节码文件,JVM执行的是.class
格式的字节码文件,Android程序在编译之后产生的.class
文件会被aapt
工具处理生成R.class
等文件,然后dx
工具会把.class
文件处理成.dex
文件,最终资源文件和.dex
文件等打包成.apk
文件。
由于Young Generation通常存活的时间比较短,所以Young Generation采用了Copying算法进行回收,Copying算法就是扫描出存活的对象,并复制到一块新的空间中,这个过程就是下图Eden与Survivor Space之间的复制过程。Young Generation采用空闲指针的方式来控制GC触发,指针保存最后一个分配在Young Generation中分配空间地对象的位置。当有新的对象要分配内存空间的时候,就会主动检测空间是否足够,不够的情况下就出触发GC,当连续分配对象时,对象会逐渐从Eden移动到Survivor,最后移动到Old Generation。
Old Generation与Young Generation不同,对象存活的时间比较长,比较稳固,因此采用标记(Mark)算法来进行回收。所谓标记就是扫描出存活的对象,然后在回收未必标记的对象。回收后的剩余空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。
从上面的一小节中我们知道了不同的区域GC机制是有所不同的,那么这些垃圾是如何被发现的呢?下面我们就看一下两种常见的判断方法:引用计数、对象引用遍历。
当一个对象已经不需要在使用了,本应该被回收,而另一个正在使用的对象持有它的引用,导致对象不能被回收。因为不能被及时回收的本该被回收的内存,就产生了内存泄漏。如果内存泄漏太多会导致程序没有办法申请内存,最后出现内存溢出的错误。
堆内存都有一定的大小,能容纳的数据是有限制的,当Java堆的大小太大时,垃圾收集会启动停止堆中不再应用的对象,来释放内存。当在极短时间内分配给对象和回收对象的过程就是内存抖动。
极短时间内分配给对象和回收对象的过程。一般多是在循环语句中创建临时对象,在绘制时配置大量对象或者执行动画时创建大量临时对象。
内存抖动会带来UI的卡顿,因为大量的对象创建,会很快消耗剩余内存,导致GC回收,GC会占用大量的帧绘制时间,从而导致UI卡顿。
使用: ComonUtil mComonUtil = ComonUtil.getInstance(this);
上面的代码就是我们平时使用的单例模式,当然这里没有考虑线程安全,请忽略。当我们传递进来的是Context,那么当前对象就会持有第一次实例化的Context,如果Context是Activity对象,那么就会产生内存泄漏。因为当前对象ComonUtil是静态的,生命周期和应用是一样的,只有应用退出才会释放,导致Activity不能及时释放,带来内存泄漏。
常见的有两种方式,第一就是传入ApplicationContext,第二CommonUtil中取context.getApplicationContext()。
同单例引起的内存泄漏。
常见的就是数据库游标没有关闭,对象文件流没有关闭,主要记得关闭就OK了。
5.5设置监听
常见的是在观察者模式中出现,我们在退出Acviity时没有取消监听,导致被观察者还持有当前Activity的引用,从而引起内存泄漏。常见的解决方法就是在onPause中注消监听
5.6使用AsyncTask
和上面同样的道理,匿名内部类持有外部类的引用,AsyncTask耗时操作导致Activity不能及时释放,引起内存泄漏。
解决方法同上:
1.声明为静态类
2.在onPause中取消任务
5.7使用Bitmap
我们知道当bitmap对象没有被使用(引用),gc会回收bitmap的占用内存,当时这边的内存指的是java层的,那么本地内存的释放呢?我们可以通过调用bitmap.recycle()来释放C层上的内存,防止本地内存泄漏 。