为什么做内存优化,优化着手点在哪里?
**关于内存优化是Android开发一直以来,经常运用的,热议的,必备的一个技术点。**我们为什么要做内存优化?好处是什么?如何去做好内存优化?
一丶内存优化好处?
做内存优化的目的是降低OOM率、减少卡顿、增加应用存活时间。
- 降低OOM率
做内存优化的一个常见原因是为了降低OOM率。
申请内存过多而没有及时释放,常常就会导致OOM。
引起OOM的原因有多种,在后面我们再细谈。
- 减少卡顿
Android中造成界面卡顿的原因有很多种,其中一种就是由内存问题引起的。
内存问题之所以会影响到界面流畅度,是因为垃圾回收。
在GC时,所有线程都要停止,包括主线程.当GC和绘制界面的操作同时触发时,绘制的执行就会被搁置,导致掉帧,也就是界面卡顿。
- 增加应用存活时间
Android会按照特定的机制清理进程,清理进程时优先会考虑清理后台进程,如果某个应用在后台运行并且占用的内存更多,就会被优先清理掉。
我们通常希望App能尽量存活的久一点,所以内存不再使用时应该尽快释放。
学习地址 :AndroidT10级高工必备性能优化合集
- 使用LargeHeap属性增加最大可用内存。
- 在系统触发资源紧张回调时,主动删除缓存。
- 使用优化过后的集合:如SparseArray类等。
- 谨慎使用 SharedPreference,SP会在应用初始化时将所有内容加载到内存中,所以不应该存放比较大的内容。
- 谨慎使用外部库,引入时需要明确不会对应用性能造成大的影响。
- 业务架构设计要合理,抽象可以优化代码的灵活性和可维护性,但是抽象也会带来其他成本,应权衡使用。
文章图片
1.物理地址与虚拟地址: 虚拟内存是程序和物理内存之间引入的中间层,目的是解决直接使用物理内存带来的安全性问题、超过物理内存大小需求无法满足等等问题。
而Linux的内存管理就是建立在虚拟内存之上的。虚拟地址与物理地址通过页表建立映射关系,CPU通过MMU(Memory Management Unit :内存管理单元)访问页表来查询虚拟地址对应的物理地址。虚拟地址分为内核空间和用户空间,它们对应的虚拟地址分别为进程共享和进程隔离的。
2.内核空间内存管理: 内核把page作为内存管理的基本单位。对特性不同的page又以zone来做划分,zone又由node来管理。
主要关注的区有3个:
区 | 描述 |
---|---|
ZONE_DMA | 直接内存访问,无需映射 |
ZONE_NORMAL | 一一对应映射页 |
ZONE_HIGHMEM | 动态映射页 |
Buddy伙伴算法以产生内部碎片为代价来避免外部碎片的产生。Linux针对大内存的物理地址分配,采用Buddy伙伴算法,如果是针对小于一个page的内存,频繁的分配和释放,则不宜用Buddy伙伴算法,取而代之的是Slab。
Slab是为频繁分配/释放的对象建立高速缓存。
3.用户空间内存管理: 用户空间主要分两部分,一个是面向C++的native层,一个是基于虚拟机的java层。
native内存划分:
- Data 用于保存全局变量
- Bss 用于保存全局未初始化变量
- Code 程序代码段
- Stack 线程函数执行的内存
- Heap malloc分配管理的内存
- Program Counter Register 它是一个指针,指向执行引擎正在执行的指令的地址。
- VM stack 基于方法中的局部变量,包括基本数据类型以及对象引用等。
- Native Method Stack 针对native方法,功能与虚拟机栈一致。
- Method Area 虚拟机加载的类信息、常量、静态变量等。
- Heap 对象实体。
文章图片
四、内存分配 在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,它们分配的内存区域块是不同的,下面我们就来简单了解下。
Dalvik
- Linear Alloc
- Zygote Space
- Alloc Space
- Non Moving Space
- Zygote Space
- Alloc Space
- Image Space
- Large Obj 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:存放一些预加载类,类似于Dalvik中的Linear Alloc。与Zygote Space一样,在Zygote进程和应用程序进程之间共享。
- Large Object Space:离散地址的集合,分配一些大对象,用于提高GC的管理效率和整体性能。
五丶内存回收机制 在Android的高级系统版本中,针对Heap空间有一个Generational Heap Memory的模型,其中将整个内存分为三个区域:
- Young Generation(年轻代)
- Old Generation(年老代)
- Permanent Generation(持久代)
文章图片
**1.**Young Generation 由一个Eden区和两个Survivor区组成,程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当此Survivor区满时,此区存活的对象又被复制到另一个Survivor区,当这个Survivor区也满时,会将其中存活的对象复制到年老代。
2.Old Generation 一般情况下,年老代中的对象生命周期都比较长。
3.Permanent Generation 用于存放静态的类和方法,持久代对垃圾回收没有显著影响。
4.内存对象的处理过程小结
- 对象创建后在Eden区。
- 执行GC后,如果对象仍然存活,则复制到S0区。
- 当S0区满时,该区域存活对象将复制到S1区,然后S0清空,接下来S0和S1角色互换。
- 当第3步达到一定次数(系统版本不同会有差异)后,存活对象将被复制到Old Generation。
- 当这个对象在Old Generation区域停留的时间达到一定程度时,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。
此外,执行GC占用的时间与Generation和Generation中的对象数量有关,如下所示:
- Young Generation < Old Generation < Permanent Generation
- Generation中的对象数量与执行时间成反比。
6.Old Generation GC 由于其对象存活时间较长,比较稳定,因此采用Mark(标记)算法(扫描出存活的对象,然后再回收未被标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减少内存碎片带来的效率损耗)来回收。
六丶GC机制概述
与C++不用,在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但也随之带来了内存泄漏的可能。简单点说:对于 C++ 来说,内存泄漏就是new出来的对象没有 delete,俗称野指针;而对于 java 来说,就是 new 出来的 Object 放在 Heap 上无法被GC回收。
Android使用的主要开发语言是Java所以二者的GC机制原理也大同小异,所以我们只对于常见的JVM GC机制的分析,就能达到我们的目的。我还是先看看那二者的不同之处吧。
1.Dalvik 和标准Java虚拟机的主要区别 Dalvik虚拟机(DVM)是Android系统在java虚拟机(JVM)基础上优化得到的,DVM是基于寄存器的,而JVM是基于栈的,由于寄存器高效快速的特性,DVM的性能相比JVM更好。
2.Dalvik 和 java 字节码的区别 Dalvik执行
.dex
格式的字节码文件,JVM执行的是.class
格式的字节码文件,Android程序在编译之后产生的.class
文件会被aapt
工具处理生成R.class
等文件,然后dx
工具会把.class
文件处理成.dex
文件,最终资源文件和.dex
文件等打包成.apk
文件。3.对于Young Generation(新生代)的GC 由于Young Generation通常存活的时间比较短,所以Young Generation采用了Copying算法进行回收,Copying算法就是扫描出存活的对象,并复制到一块新的空间中,这个过程就是下图Eden与Survivor Space之间的复制过程。Young Generation采用空闲指针的方式来控制GC触发,指针保存最后一个分配在Young Generation中分配空间地对象的位置。当有新的对象要分配内存空间的时候,就会主动检测空间是否足够,不够的情况下就出触发GC,当连续分配对象时,对象会逐渐从Eden移动到Survivor,最后移动到Old Generation。
文章图片
4.对于Old Generation(旧生代)的GC Old Generation与Young Generation不同,对象存活的时间比较长,比较稳固,因此采用标记(Mark)算法来进行回收。所谓标记就是扫描出存活的对象,然后在回收未必标记的对象。回收后的剩余空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。
5.如何判断对象是否可以被回收 从上面的一小节中我们知道了不同的区域GC机制是有所不同的,那么这些垃圾是如何被发现的呢?下面我们就看一下两种常见的判断方法:引用计数、对象引用遍历。
6.引用计数器
- 引用计数器是垃圾收集器中的早起策略。这种方法中,每个对象实体(不是它的引用)都有一个引用计数器。当一个对象创建的时候,且将该对象分配给一个每分配给一个变量,计数器就+1,当一个对象的某个引用超过了生命周期或者被设置一个新值时,对象计数器就-1,任何引用计数器为 0 的对象可以被当作垃圾收集。当一个对象被垃圾收集时,引用的任何对象技术 - 1。
- 优点:执行快,交织在程序运行中,对程序不被长时间打断的实时环境比较有利。
- *缺点:*无法检测出循环引用。比如:对象A中有对象B的引用,而B中同时也有A的引用。
- 现在的垃圾回收机制已经不太使用引用计数器的方法判断是否可回收,而是使用跟踪收集器方法。
- 现在大多数JVM采用对象引用遍历机制从程序的主要运行对象(如静态对象/寄存器/栈上指向的堆内存对象等)开始检查引用链,去递归判断对象收否可达,如果不可达,则作为垃圾回收,当然在便利阶段,GC必须记住那些对象是可达的,以便删除不可到达的对象,这称为标记(marking)对象。
- 下一步,GC就要删除这些不可达的对象,在删除时未必标记的对象,释放它们的内存的过程叫做清除(sweeping),而这样会造成内存碎片化,布局已分配给新的对象,但是他们集合起来还很大。所以很多GC机制还要重新组织内存中的对象,并进行压缩,形成大块、可利用的空间。
- 为了达到这个目的,GC需要停止程序的其他活动,阻塞进程。这里我们要注意的是:不要频繁的引发GC,执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行, 故而如果程序频繁GC, 自然会导致界面卡顿. 通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。
1.什么是内存泄漏? 当一个对象已经不需要在使用了,本应该被回收,而另一个正在使用的对象持有它的引用,导致对象不能被回收。因为不能被及时回收的本该被回收的内存,就产生了内存泄漏。如果内存泄漏太多会导致程序没有办法申请内存,最后出现内存溢出的错误。
2.什么是内存抖动? 堆内存都有一定的大小,能容纳的数据是有限制的,当Java堆的大小太大时,垃圾收集会启动停止堆中不再应用的对象,来释放内存。当在极短时间内分配给对象和回收对象的过程就是内存抖动。
3.内存抖动产生的原因? 极短时间内分配给对象和回收对象的过程。一般多是在循环语句中创建临时对象,在绘制时配置大量对象或者执行动画时创建大量临时对象。
内存抖动会带来UI的卡顿,因为大量的对象创建,会很快消耗剩余内存,导致GC回收,GC会占用大量的帧绘制时间,从而导致UI卡顿。
4.导致内存泄漏的主要几个点
- 使用单例模式
- 使用匿名内部类
- 使用异步事件处理机制Handler
- 使用静态变量
- 资源未关闭
- 设置监听
- 使用AsyncTask
- 使用Bitmap
文章图片
使用: ComonUtil mComonUtil = ComonUtil.getInstance(this);
上面的代码就是我们平时使用的单例模式,当然这里没有考虑线程安全,请忽略。当我们传递进来的是Context,那么当前对象就会持有第一次实例化的Context,如果Context是Activity对象,那么就会产生内存泄漏。因为当前对象ComonUtil是静态的,生命周期和应用是一样的,只有应用退出才会释放,导致Activity不能及时释放,带来内存泄漏。
解决方法? 常见的有两种方式,第一就是传入ApplicationContext,第二CommonUtil中取context.getApplicationContext()。
文章图片
5.2 使用异步事件处理机制Handler
文章图片
- 这个应该是我们平时使用最多的一种方式,如果当handler中处理的是耗时操作,或者当前消息队列中消息很多时,那当Activity退出时,当前message中持有handler的引用,handler又持有Activity的引用,导致Activity不能及时的释放,引起内存泄漏的问题。
解决handler引起的内存泄漏问题常用的两种方式:
和上面解决Thread的方式一样;
在onDestroy中调用mHandler.removeCallbacksAndMessages(null)。
文章图片
5.3使用静态变量 同单例引起的内存泄漏。
5.4资源未关闭 常见的就是数据库游标没有关闭,对象文件流没有关闭,主要记得关闭就OK了。
5.5设置监听
常见的是在观察者模式中出现,我们在退出Acviity时没有取消监听,导致被观察者还持有当前Activity的引用,从而引起内存泄漏。常见的解决方法就是在onPause中注消监听
5.6使用AsyncTask
文章图片
和上面同样的道理,匿名内部类持有外部类的引用,AsyncTask耗时操作导致Activity不能及时释放,引起内存泄漏。
解决方法同上:
1.声明为静态类
2.在onPause中取消任务
5.7使用Bitmap
【Android开发|为什么做内存优化,优化着手点在哪里()】我们知道当bitmap对象没有被使用(引用),gc会回收bitmap的占用内存,当时这边的内存指的是java层的,那么本地内存的释放呢?我们可以通过调用bitmap.recycle()来释放C层上的内存,防止本地内存泄漏 。
八.尾述:
- Android内存优化主要是针对堆(Heap)而言的,当堆中对象的作用域结束的时候,这部分内存也不会立刻被回收,而是等待系统GC进行回收。
- Java中造成内存泄漏的根本原因是:堆内存中长生命周期的对象持有短生命周期对象的强/软引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。
自己也是从事Android开发5年有余了;整理了一些Android开发技术核心笔记:
T10级工程师必会的Android优化解析
想有收获必须有努力,知识在于积累。不进则退。
推荐阅读
- Android开发|我的Android网络优化为什么不行()
- Android开发|ASM与JAVASSIST区别在哪()
- Android布局|如何实战一个Android UI布局,这篇带你玩转UI
- Android|『查漏补缺』Android实习面试知识点(二)
- Android|『查漏补缺』Android实习面试知识点(一)
- Android|『基础巩固』---清晰图解深度分析HTTPS原理
- Android|Android2023暑期实习---网易游戏一面面经
- Android|Android----banner使用详解
- 游戏|一个niubility的Vue游戏,真厉害!