Android 性能优化 -- 内存优化
java 对象的生命周期
主要分7个阶段
- Created : 创建阶段
主要分以下几个步骤- 为对象分配存储空间
- 构造对象
- 从父类到子类对static成员进行初始化,类的static成员初始化在ClassLoader加载该类的时候进行
- 父类成员变量按照顺序初始化,递归调用超类的构造方法
- 子类成员变量按照顺序初始化,一旦对象被创建,子类的构造方法就调用该对象并未某些变量赋值
- In Use : 应用阶段
此时对象至少被一个强引用持有。 - InVisible : 不可见阶段
当一个对象处于不可见阶段,说明程序本身不再持有该对象的任何强引用,虽然该对象仍然是存在的。
简单的说就是程序的执行已经超出了该对象的作用域,但是该对象仍然可能被某些已经装载的静态变量线程或者JNI等强引用持有,这些特殊的强引用称为 GC ROOT。被这些GC ROOT强引用的对象会导致该对象的内存泄漏,因而无法被GC回收。 - UnReachable : 不可达阶段
该对象不再被任何强引用持有。 - Collected : 收集阶段
当GC已经对该对象的空间重新分配做好了准备的时候,对象进入收集阶段,如果该对象重写了 finalize(),则执行它。 - Finalized : 终结阶段
等待垃圾回收器回收该对象空间。 - Deallocated : 对象重新分配阶段
GC对该对象所占用的内存空间进行回收或者再分配,则该对象彻底消失。
用一张图片表示如下
内存分配策略
- 对象/变量的内存分配由程序自动负责
- 共3种,静态分配,栈式分配,和堆式分配,分别指向 静态变量,局部变量和对象实例
名词解释
- 寄存器: 速度最快的存储场所。因为寄存器处于 CPU 内部,在程序中无法控制。
- 栈(Stack):存放基本类型的数据和对象引用。但对象本身不存放在栈中,而是堆中
- 堆(Heap): 存放由 new 创建的对象和数组。在堆中分配的内存,由 Java 虚拟机的 GC 来管理,JVM 所管理的内存中最大的一块
- 静态存储区域(Static Field): 在固定的位置存放应用程序运行试一直存在的数据, Java 在内存中专门划分了一个静态存储区域来管理一些特殊的数据变量,如静态的数据变量
- 常量池(Constant Pool): JVM必须为每一个被装载的类型维护一个常量池。常量池就是该类型所有用到的常量的有序集合,包括直接常量(基本类型,String)和对其他类型,字段和方法的符号引用。
JVM 将内存划分为几块,分别如下所示
- 方法区: 存储类信息,常量,静态变量等。 所有线程共享
- 虚拟机栈:存储局部变量表,操作数栈等,与线程的生命周期同步,对这个区域规定了两种异常状况
- StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。
- OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出。
- 本地方法栈:不同与虚拟机栈为Java方法服务,他是为Native方法服务。
- 堆: 内存中最大的一块区域,每一个对象实际分配的内存都在堆上 ,而在虚拟机栈中分配的只是引用。这些引用会指向真正存储的对象。此外,堆也是GC所主要作用的区域。并且内存泄漏也是发生在这个区域。 所有线程共享
- 程序计数器: 存储当前线程执行目标方法执行到了第几行。
如下图
举例
public class Sample {
// 该类的实例对象的成员变量s1、mSample1 指向对象存放在堆内存中
int s1 = 0;
Sample mSample1 = new Sample();
// 方法中的局部变量s2、mSample2存放在 栈内存
// 变量mSample2所指向的对象实例存放在 堆内存
public void method() {
int s2 = 0;
Sample mSample2 = new Sample();
}
}
// 变量mSample3的引用存放在栈内存中,然而变量mSample3所指向的对象实例存放在堆内存
Sample mSample3 = new Sample();
GC算法
主要包括
标记-清除算法
- 实现原理: 标记出所有需要回收的对象。然后统一回收所有被标记的对象
- 特点
- 标记和清除效率不高。
- 产生大量不连续的内存碎片。
复制算法
- 实现原理: 将内存划分为大小相等的两块。一块内存用完之后复制存活对象至另一块。清理这一块内存。
- 特点
- 实现简单,运行高效。
- 浪费一半空间,代价大。
标记-整理算法
- 实现原理:记过程与 ”标记-清除“ 算法一样。存活对象往一端进行移动。清理其余内存。
- 特点
- 避免 ”标记-清除” 算法导致的内存碎片。
- 避免复制算法的空间浪费。
分代收集算法
大多数虚拟机厂商所选用的算法
- 特点
- 结合多种收集算法的优势。
- 新生代对象存活率低 => “复制” 算法(注意这里每一次的复制比例都是可以调整的,如一次仅复制 30% 的存活对象)。
- 老年代对象存活率高 => “标记-整理” 算法。
详情查看 扫盲系列 - JVM 的垃圾回收
Low Memory Killer 机制
LMK 机制是针对于手机系统所有进程而制定的,当我们手机内存不足的情况下,LMK 机制就会针对我们所有进程进行回收,而其对于不同的进程,它的回收力度也是有不同的,目前系统的进程类型主要有如下几种:
- 前台进程
- 可见进程
- 服务进程
- 后台进程
- 空进程
从前台进程到空进程,进程优先级会越来越低,因此,它被 LMK 机制杀死的几率也会相应变大。此外,LMK 机制也会综合考虑回收收益,这样就能保证我们大多数进程不会出现内存不足的情况。
内存泄漏
详情: Android 内存泄漏总结
图片Bitmap 相关
内存抖动
定义: 内存大小不断浮动的现象
-
原因:
程序频繁的分配内存或者GC频繁的回收内存。主要表现在大量临时的小对象频繁创建 - 后果:
- GC频繁的回收导致卡顿,甚至内存溢出OOM
- 大量临时的对象频繁创建,会导致内存碎片,使得当需要内存分配的时候,虽然总体上还有剩余内存可分配,但是由于这些内存不连续,导致无法整块分配 系统则视为内存不够,出现OOM
- 优化方案
尽量避免频繁创建大量,临时的对象
代码质量和数量
代码本身的质量(如数据结构,数据类型等)和数量(代码量的大小)可能导致大量的内存问题,如占用内存大,内存利用率低等,主要可以从以下几个方面入手
- 减少不必要的类,方法,库,并且使用混淆
- 使用性能更高的数据结构。例如。SparseArray,SparseBooleanArray,LongSparseArray,
- SparseArray 就避免掉了基本数据类型转换成对象数据类型的时间。
- ArrayMap提供了和HashMap一样的功能,但避免了过多的内存开销,方法是使用两个小数组,而不是一个大数组。并且ArrayMap在内存上是连续不间断的。
- 使用占内存小是数据结构,避免使用枚举类型
- 根据不同的应用场景,使用不同的引用类型。关于Java引用类型,请查看: 扫盲系列 - Java 引用类型
- 减少AutoBoxing。自动装箱的核心就是把基础数据类型转换成对应的复杂类型。在自动装箱转化时,都会产生一个新的对象,这样就会产生更多的内存和性能开销。如int只占4字节,而Integer对象有16字节,特别是HashMap这类容器,进行增、删、改、查操作时,都会产生大量的自动装箱操作。
- 检测方式: 使用TraceView查看耗时,如果发现调用了大量的integer.value,就说明发生了AutoBoxing。
其他优化技巧
- 调大 虚拟机Dalvik的堆内存大小
即 在AndroidManifest.xml的application标签中增加一个android:largeHeap属性(值 = true),从而通知虚拟机 应用程序需更大的堆内存。但不建议不鼓励该做法 - 获取当前可使用的内存大小
调用 ActivityManager.getMemoryClass()方法可获取当前应用可用的内存大小(单位 = 兆) - 获取当前的内存使用情况
在应用生命周期的任何阶段,调用 onTrimMemory()获取应用程序, 当前内存使用情况(以内存级别进行识别),可根据该方法返回的内存紧张级别参数 来释放内存。 - 当视图变为隐藏状态时,则释放内存
当用户跳转到不同的应用/视图不再显示时, 应释放应用视图所占的资源 - 内存复用
- 资源复用:通用的字符串、颜色定义、简单页面布局的复用。
- 视图复用:可以使用ViewHolder实现ConvertView复用。
- 对象池:显示创建对象池,实现复用逻辑,对相同的类型数据使用同一块内存空间。
- Bitmap对象的复用:使用inBitmap属性可以告知Bitmap解码器尝试使用已经存在的内存区域,新解码的bitmap会尝试使用之前那张bitmap在heap中占据的pixel data内存区域。
- item被回收不可见时释放掉对图片的引用
内存优化的意义
- 减少OOM,提高应用稳定性。
- 减少卡顿,提高应用流畅度。
- 减少内存占用,提高应用后台运行时的存活率。
- 减少异常发生和代码逻辑隐患。
搬运地址:
Android 工程师进阶 34 讲
既已览卷至此,何不品评一二: